reactjstypescriptreact-hoc

React HOC type inferring


I would like to create a higher-order function that returns a higher-order component.

In the example below I've created a validateSearchParams function, which creates a function which then can be used to wrap a component. However, Typescript (3.7.2) cannot infer the types correctly.

export function validateSearchParams<Props extends { location: Location }>(
  test: (searchParams: URLSearchParams) => boolean,
  redirectTo: string,
): (Cmp: React.ComponentType<Props>) => React.ComponentType<Props> {
  return (Cmp) => (props) => {
    const searchParams = new URLSearchParams(props.location.search);

    if (!test(searchParams)) {
      return <Redirect to={redirectTo} />;
    }

    return <Cmp {...props} />;
  };
}

export const Test: React.FC<{ something: boolean; location: Location }> = () => null;

// This is OK
validateSearchParams<{ something: boolean; location: Location }>(
  (searchParams) => !!searchParams.get('foo'),
  'hxxp://somewhere',
)(Test);

// ...but without an explicit type it does not compile
validateSearchParams((searchParams) => !!searchParams.get('foo'), 'http://test.example')(Test);
                                                                                         ^^^^

In the last line I get a following error:

Argument of type 'FC<{ something: boolean; location: Location; }>' is not assignable to
parameter of type 'ComponentType<{ location: Location; }>'.
  Type 'FC<{ something: boolean; location: Location; }>' is not assignable to type
'FunctionComponent<{ location: Location; }>'.
    Types of parameters 'props' and 'props' are incompatible.
      Type 'PropsWithChildren<{ location: Location; }>' is not assignable to type
'PropsWithChildren<{ something: boolean; location: Location; }>'.
        Property 'something' is missing in type 'PropsWithChildren<{ location: Location; }>' but
required in type '{ something: boolean; location: Location; }'.ts(2345)

I am also able to create an easier version of a HOC, by incorporating the Cmp into parameters list:

export function validateSearchParams2<Props extends { location: Location; match: match<any> }>(
  Cmp: React.ComponentType<Props>,
  test: (searchParams: URLSearchParams) => boolean,
  redirectTo: string,
): React.ComponentType<Props> {
  return (props) => {
    const searchParams = new URLSearchParams(props.location.search);

    if (!test(searchParams)) {
      return <Redirect to={redirectTo} />;
    }

    return <Cmp {...props} />;
  };
}
export const Test: React.FC<{ something: boolean; location: Location }> = () => null;

validateSearchParams2(Test, (searchParams) => !!searchParams.get('foo'), 'hxxp://somewhere');

...but is there a way to make the first version of the validateSearchParams work without an explicit type?


Solution

  • Right now the generic type Props is set on the function validateSearchParams. You want to move the generic to to the returned function. Basically validateSearchParams is not a generic function, but it returns a generic HOC.

    export function validateSearchParams(
      test: (searchParams: URLSearchParams) => boolean,
      redirectTo: string,
    ): <Props extends { location: Location }>(Cmp: React.ComponentType<Props>) => React.ComponentType<Props> {
    ...
    

    Now you do not need to set an explicit type since it can be inferred from the component that you call it with.

    Typescript Playground Link