Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to define generic HOC with additional interface to add to props

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.

like image 331
Konrad Klimczak Avatar asked Dec 18 '25 05:12

Konrad Klimczak


2 Answers

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

like image 152
Aviad Hadad Avatar answered Dec 19 '25 17:12

Aviad Hadad


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:

  • You don't get a weird display name (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.
  • The resulting component is as flexible as any other React component. Just add logic to the function body if you find you need it. But with HOCs there is no function body. You could add more than 1 HOC but that gets unwieldy very quickly.

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);
like image 24
inwerpsel Avatar answered Dec 19 '25 18:12

inwerpsel



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!