HOC returned component props can not differ from HOC generic props #28938
Copy link
Copy link
Open
Labels
BugA bug in TypeScriptA bug in TypeScriptDomain: JSX/TSXRelates to the JSX parser and emitterRelates to the JSX parser and emitter
Milestone
Description
TypeScript Version: 3.3.0-dev.20181208
Search Terms:
- HOC
- react
- higher order component
Code
import * as React from 'react';
export interface HOCProps {
foo: number;
}
/** Remove props, that have been prefilled by the HOC */
type WithoutPrefilled<T extends HOCProps> = Pick<T, Exclude<keyof T, 'foo'>>;
function withFoo<P extends HOCProps>(WrappedComponent: React.ComponentType<P>) {
return class SomeHOC extends React.Component<WithoutPrefilled<P>> {
public render(): JSX.Element {
return <WrappedComponent {...this.props} foo={0} />;
}
};
}Expected behavior:
No error, like with every version below 3.2.0.
Actual behavior:
Throws an error highlighting the WrappedComponent in the render method.
[ts]
Type 'Readonly<{ children?: ReactNode; }> & Readonly<Pick<P, Exclude<keyof P, "foo">>> & { foo: number; }' is not assignable to type 'IntrinsicAttributes & P & { children?: ReactNode; }'.
Type 'Readonly<{ children?: ReactNode; }> & Readonly<Pick<P, Exclude<keyof P, "foo">>> & { foo: number; }' is not assignable to type 'P'. [2322]
Additional Information
This is pretty much the same sample example used in #28720, but with the difference, that the props of the returned component differ from the generic.
Basically the HOC prefills the foo property for the WrappedComponent. Since the spreaded props are overriden by foo, I don’t want foo to be a valid property for the HOC. This does not seem to be possible anymore.
Playground Link:
Related Issues:
#28720
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
BugA bug in TypeScriptA bug in TypeScriptDomain: JSX/TSXRelates to the JSX parser and emitterRelates to the JSX parser and emitter
Activity
flsilva commentedon Dec 10, 2018
I believe I'm having the exact same problem on TypeScript 3.2.2, and I'd like to share my use case to show another way this is affecting HOCs in React. In my case I have a HOC that grabs
WrappedComponent'schildrento render it in a different way, forwarding all props but children toWrappedComponent, resulting in the exact same error Henning Pohlmeyer (@hpohlmeyer) mentioned.Here's a simplified (React Native) code:
Error:
Type '{ children: Element; }' is not assignable to type 'P'. [2322]This was working fine on 3.0.1 and is now giving me this error on 3.2.2 (haven't tested on 3.1).
In my real world case I need
<P extends ViewProps>to have the ability to useonLayout:<WrappedComponent {...otherProps} onLayout={this.myFunction} />If I forward all HOC props to WrappedComponent the error is gone, but that would prevent HOCs to change behavior of WrappedComponents.
No errors in the above code.
SpiderSkippy commentedon Dec 10, 2018
I'm seeing the same thing as the original poster, but from version 3.1.6 to 3.2.2
weswigham commentedon Dec 10, 2018
Anders Hejlsberg (@ahejlsberg) The change is that we no longer erase generics in JSX (so we actually check these calls now), and roughly that
Pick<P, Exclude<keyof P, "foo">> & { foo: P["foo"] }doesn't recombine to (or get recognized as assignable to)P. It's an unfortunate interaction with generic rest/spread, and the error we output is bad, too.port hoc.js to typescript
hpohlmeyer commentedon Dec 11, 2018
Just for the sake of completeness: In some cases we do not remove the prop from the returned component, but make it optional instead. This also fails for the same reason:
Error:
So basically it’s the same. I just wanted to provide another example where TypeScript does not recognize that the type is assignable to T. This could be harder to solve then the original one though.
sveyret commentedon Dec 15, 2018
It looks like the same problem as in #28884. It fails even if no spread rest.
ChristianIvicevic commentedon Dec 31, 2018
Starting with 3.2 the behaviour of the spread operator for generics has changed. Apparently the type of
propsgets erased as a negative side effect, but you can work around that by casting it back toPusing{...props as P}when spreading back into the wrapped component.OliverJAsh commentedon Jan 21, 2019
Christian Ivicevic (@ChristianIvicevic)
Is there a bug somewhere where we can track this specific problem?
OliverJAsh commentedon Jan 22, 2019
Wesley Wigham (@weswigham) Is this the same as #28748?
aprilandjan commentedon Jan 25, 2019
Had to use
<WrappedComponent {...this.props as any} inject={injected}/>to temporarily bypass the HOC props merge.Fix hoc typing
Migrate www to typescript (#1537)
4 remaining items
satansdeer commentedon Mar 3, 2021
I've tried to do this to avoid type assertion:
This way the wrapped component can't receive the injected props and TypeScript does not complain.
Here is a TypeScript playground with an example of how this HOC can be used.
gabriellsh commentedon Mar 4, 2021
Maksim Ivanov (@satansdeer) but you're not getting the rest props, it should be
return ({somekey, ...props}) =>....satansdeer commentedon Mar 4, 2021
gabriellsh not sure why do you need to get the
restas you are overriding thefooprop in my example. React will accept the last value of the prop that was passed, here is a codesandbox.satansdeer commentedon Mar 4, 2021
Ok here is another take from Artem Samofalov (@dex157):
This way we don't even have to use
Omitand type inference works correctly:Here is a TypeScript playground to try it out.
coalaroot commentedon Apr 8, 2021
Basically the difference in Maksim Ivanov (@satansdeer) answer is to use type (which is not stated there explicitly) instead of interfaces
This will not work
And this will
The difference is in ButtonProps
satansdeer commentedon Apr 11, 2021
Jakub Królak (@coalaroot) great catch. Yes, using the
extendssyntax is not the same as having an intersection type.Omelyan commentedon Apr 27, 2023
will... how about generics in FooProps? :))
coalaroot commentedon Apr 27, 2023
Can do but why though?
coalaroot commentedon Apr 27, 2023
Sorry man, don't have the time to figure it out :(
hayata-suenaga commentedon Jul 6, 2023
This is the simplest reproducible sample code
This raises the following error:
lifeiscontent commentedon Sep 4, 2024
Hayata Suenaga (@hayata-suenaga) weird thing is, this works:
keropodium commentedon May 23, 2025
In my case I had to go from this:
That raised:
To extend the generic type and then explicitly omit on the wrapped:
RyanCavanaugh commentedon Sep 1, 2026
This error is correct.
P extends HOCPropsdoes not mean thatPis exactlyHOCProps; it can require a narrowerfootype. For example,Pcould be{ foo: 1; label: string }. The HOC suppliesfoo={0}, so the props assembled fromWithoutPrefilled<P>and{ foo: number }are not safe to pass to every possibleReact.ComponentType<P>.This is the distinction described in Why can't I narrow or assign to a generic type parameter the way I can with its constraint?. TypeScript 3.2 correctly began checking the generic JSX call instead of erasing its type parameter.