reactjstypescriptgenericshigher-order-components

Generics Inference when overriding a prop in higher order component


I have the following code:

type XType<T> = {
  x: T;
};
function extractTypes<Props, T>(Component: React.FC<Props & XType<T>>) {
  return (props: Props, value: T) => {
    Component({ ...props, x: value });
  };
}


const CompA: React.FC<{ a: string } & XType<string>> = () => <div>HELLO</div>;
const CompB: React.FC<{ b: string } & XType<number>> = () => <div>WORLD</div>;


extractTypes<{ a: string }, string>(CompA)({ a: 'A' }, 'X'); // OK
extractTypes<{ b: string }, number>(CompB)({ b: 'B' }, 1);   // OK

extractTypes(CompA)({ a: 'A' }, 'X');                        // Compile error
extractTypes(CompB)({ b: 'B' }, 1);                          // Compile error

CompA and CompB expect a and b properties along with all properties in XType.

The 3rd-last and 4th-last compile correctly, however the Typescript compiler complains on the last two lines as follows:

Argument of type '{ a: string; }' is not assignable to parameter of type 'PropsA & XType<string>'.
  Property 'x' is missing in type '{ a: string; }' but required in type 'XType<string>'.ts(2345)

Argument of type '{ b: string; }' is not assignable to parameter of type '{ b: string; } & XType<number>'.
  Property 'x' is missing in type '{ b: string; }' but required in type 'XType<number>'.ts(2345)

I've tried many approaches to ensure that I can call extractTypes without specifying the types explicitly and still ensure that the method returned by this function accepts only { a: string } or { b: string } instead of { a: string; x: string } or { b: string; x: number }, but to no avail.

Is there any way I can accomplish this without requiring explicit specifying generic types for extractTypes?

I think I need to find a way to create a type by subtracting one type (XType) from another (Props). I've tried using Omit<Props, keyof XType<T>> but it doesn't work well here.

Thanks!

Asim


Solution

  • I would simply extend XType. This will force you to assert a type internally though.

    // adding a default for convenience
    type XType<T = unknown> = {
      x: T;
    };
    
    function extractTypes<Props extends XType>(Component: React.FC<Props>) {
      // simply index and omit keys in `Props`
      return (props: Omit<Props, keyof XType>, value: Props['x']) => {
        Component({ ...(props as Props), x: value });
        //    assertion --------------
      };
    }
    

    playground

    If you want excess property checking to also work when you pass a reference as props, you can use Omit<Props, keyof XType> & {[k in keyof XType]?: never} but this may be overkill given the way people use React.


    As a side-note, merging the 2 generics was not strictly necessary and your original design is safer, because removing x: value will yield an error

     function extractTypes<Props, T>(Component: React.FC<Props & XType<T>>) {
       return (props: Omit<Props, keyof XType>, value: T) => {
         Component({ ...(props as Props), x: value });
        //    assertion ---------------
       };
     }
    

    I don't know why though because Props is inferred as {a: string} & XType<string