Remove prop injected by HOC from the Typescript interface of the component returned by HOC?

1.1k Views Asked by At

I'm trying to make a Higher Order Component which takes a function from the current context and inject it into a prop in the wrapped component, and still maintain the Props interfaces.

I wrap it like this:

interface Props extends AsyncRequestHandlerProps {
  bla: string;
}

class MyComponent extends React.Component<Props> {
 // ....
}

export default withAsyncRequestHandler(MyComponent)

And I have defined the withAsyncRequestHandler like this

export interface AsyncRequestHandlerProps {
  asyncRequestHandler: <T>(promise: Promise<T>) => Promise<T | null>;
}

type PropsWithoutInjectedHandler<P> = Omit<P, keyof AsyncRequestHandlerProps>;


export function withAsyncRequestHandler<P>(Component: React.ComponentType<P>) {
  return class ComponentWithAsyncRequestHandler extends React.Component<
    PropsWithoutInjectedHandler<P>
  > {
    static contextType = AsyncHandlerContext;
    context!: AsyncHandlerContext | null;
    render = () => {
      const asyncRequestHandler: <T>(
        promise: Promise<T>
      ) => Promise<T | null> = (promise) => {
        if (this.context === null) {
          throw new Error(
            "withAsyncRequestHandler should only wrap components that are mounted inside <AsyncHandler />."
          );
        }
        return AsyncRequest(promise, this.context);
      };
      const { ...props } = this.props;
      return (
        <Component
          {...props}
          asyncRequestHandler={asyncRequestHandler}
        ></Component>
      );
    };
  };
}

The immediate signature of MyComponent is something with both the bla prop and the asyncRequestHandler props. What I want is that the wrapper HOC will return a component signature with only the bla prop, as the asyncRequestHandler has been injected.

The external interface of this HOC seems to work, I can stil get the remaining props from typescript when mounting the wrapped components.

But internally in the HOC I get an error:

My current code gives this error, on the line where I mount the <Component> in render().

Type 'Readonly<Pick<P, Exclude<keyof P, "asyncRequestHandler">>> & { asyncRequestHandler: <T>(promise: Promise<T>) => Promise<T | null>; children?: ReactNode; }' is not assignable to type 'IntrinsicAttributes & P & { children?: ReactNode; }'.
  Type 'Readonly<Pick<P, Exclude<keyof P, "asyncRequestHandler">>> & { asyncRequestHandler: <T>(promise: Promise<T>) => Promise<T | null>; children?: ReactNode; }' is not assignable to type 'P'.
    'P' could be instantiated with an arbitrary type which could be unrelated to 'Readonly<Pick<P, Exclude<keyof P, "asyncRequestHandler">>> & { asyncRequestHandler: <T>(promise: Promise<T>) => Promise<T | null>; children?: ReactNode; }'.ts(2322)

I guess the problem lies around the Omit<P, keyof AsyncRequestHandlerProps> construction, and the usage of it?

1

There are 1 best solutions below

0
On BEST ANSWER

According to https://github.com/Microsoft/TypeScript/issues/28938#issuecomment-450636046 this is a bug in TS.

Starting with 3.2 the behaviour of the spread operator for generics has changed. Apparently the type of props gets erased as a negative side effect, but you can work around that by casting it back to P using {...props as P} when spreading back into the wrapped component.

So as suggested, try this:

<Component
      {...props as P}
      asyncRequestHandler={asyncRequestHandler}
/>