I need to create a generic HOC with will accpect an interface which will be added to component props.
I have implemented following function, but it requires two arguments instead of one. I want second argument to be taken from the Component that it's passed into the function.
export const withMoreProps = <NewProps, Props>(WrappedComponent: React.FC<Props>): React.FC<Props & NewProps> => {
const displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
const ComponentWithMoreProps = (props: Props & NewProps) => <WrappedComponent {...props} />;
ComponentWithMoreProps.displayName = `withMoreProps(${displayName})`;
return ComponentWithMoreProps;
};
Currently when I try to use this:
const Button = (props: { color: string }) => <button style={{ color: props.color }}>BTN</button>;
export const Button2 = withMoreProps<{ newProperty: string }>(Button);
I get this error message
Expected 2 type arguments, but got 1.
It should work like styled-components, where you can define only additional props.
export const StyledButton = styled(Button)<{ withPadding?: boolean }>`
padding: ${({ withPadding }) => (withPadding ? '8px' : 0)};
`;
EDIT:
This is simplified version of HOC I have created in application. The real HOC is much more complex and does other stuff, but for sake of simplification I made this example to focus only on the problem I run into.
In general, you want to use the infer keyword. You can read more about it here, but in short you can think of it as a helper to "extract" a type out of a generic type.
Let's define a type that extract the prop type out of a react component.
type InferComponentProps<T> = T extends React.FC<infer R> ? R : never;
example on what it does:
const Button = (props: { color: string }) => <button style={{ color: props.color }}>BTN</button>;
type ButtonProps = InferComponentProps<typeof Button>; // hover over ButtonProps, see {color: string}
Now that we have this "helper type", we can move on to implement what you want - but we do run into an issue. When calling a generic function in typescript, you can't specify some of the types, and some no. You either specify all the concrete types matching for this function call, or specify none, and let Typescript figure out the types.
function genericFunction<T1, T2>(t1: T2) {
//...
}
const a = genericFunction('foo') //ok
const b = genericFunction<number, string>('foo') //ok
const c = genericFunction<string>('foo') //error
You can track the typescript issue here.
So to solve this we need to do a small change to your code and do a function that returns a function that returns the new component. If you notice, it's exactly how styled works also, as using tagged template literals is really a function call. So there are 2 function calls in the styled components code you posted above.
so the final code looks something like this:
export const withMoreProps =
<C extends React.FC<any>>(WrappedComponent: C) =>
<NewProps extends Object>(): React.FC<InferComponentProps<C> & NewProps> => {
const displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
//need to re-type the component
let WrappedComponentNew = WrappedComponent as React.FC<InferComponentProps<C> & NewProps>;
const ComponentWithMoreProps = (props: InferComponentProps<C> & NewProps) => <WrappedComponentNew {...props} />;
ComponentWithMoreProps.displayName = `withMoreProps(${displayName})`;
return ComponentWithMoreProps;
};
const Button = (props: { color: string }) => <button style={{ color: props.color }}>BTN</button>;
export const Button2 = withMoreProps(Button)<{ newProperty: string }>(); //Notice the function call at the end
If you just want a generic way to add more props to a component, you don't need the overhead of a HOC for this. You can easily achieve this using the rest and spread operator to pass on the props. (I use color on the HOC here unlike OP's example where it's on the main button, it's just a nice example)
const ColoredButton = ({color, ...other}) => <Button {...other, style: {color}/>
It's maybe slightly more code than the HOC version that basically handles passing on ...other for you. However in return:
withMoreProps(Button) which could be any props). Instead it will just use the function name like any other React component (e.g. ColoredButton). You'd rather have the latter while debugging.Similarly, your issue with declaring the types simply goes away. It works exactly the same like the main button type.
const Button = (props: { color: string }) => <button style={{ color: props.color }}>BTN</button>;
export const Button2 = ({ newProperty: string, ...other }) => <Button {...other, newProperty}/>
// Original for comparison.
export const Button3 = withMoreProps<{ newProperty: string }>(Button);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With