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?
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}
/>