TypeScript Version: 3.3.0-dev.20181208
Search Terms:
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 https://github.com/Microsoft/TypeScript/issues/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鈥檛 want foo
to be a valid property for the HOC. This does not seem to be possible anymore.
Playground Link:
Related Issues:
https://github.com/Microsoft/TypeScript/issues/28720
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
's children
to render it in a different way, forwarding all props but children to WrappedComponent
, resulting in the exact same error @hpohlmeyer mentioned.
Here's a simplified (React Native) code:
import * as React from 'react';
import { ViewProps } from 'react-native';
export const withXBehavior = <P extends ViewProps>(
WrappedComponent: React.ComponentType<P>,
): React.ComponentType<P> => (props) => {
const { children, ...otherProps } = props;
return (
<WrappedComponent {...otherProps}> /* ERROR HERE */
<React.Fragment>
<View>
<Text>example of HOC stuff here</Text>
</View>
{children}
</React.Fragment>
</WrappedComponent>
);
};
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 use onLayout
:
<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.
import * as React from 'react';
import { Text, View, ViewProps } from 'react-native';
export const withXBehavior = <P extends ViewProps>(
WrappedComponent: React.ComponentType<P>,
): React.ComponentType<P> => (props) => {
const { children } = props;
return (
<WrappedComponent {...props}>
<React.Fragment>
<View>
<Text>example of HOC stuff here</Text>
</View>
{children}
</React.Fragment>
</WrappedComponent>
);
};
No errors in the above code.
I'm seeing the same thing as the original poster, but from version 3.1.6 to 3.2.2
@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.
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:
import * as React from 'react';
export interface HOCProps {
foo: number;
}
/** From T make every property K optional */
type Partialize<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>> & Partial<Pick<T, K>>;
/** Remove props, that have been prefilled by the HOC */
type OptionalPrefilled<T extends HOCProps> = Partialize<T, keyof HOCProps>;
export default function<P extends HOCProps>(WrappedComponent: React.ComponentType<P>) {
return class SomeHOC extends React.Component<OptionalPrefilled<P>> {
public render(): JSX.Element {
return <WrappedComponent foo={0} {...this.props} />;
}
};
}
Error:
[ts]
Type 'Readonly<{ children?: ReactNode; }> & Readonly<Partialize<P, "foo">> & { foo: number; }' is not assignable to type 'IntrinsicAttributes & P & { children?: ReactNode; }'.
Type 'Readonly<{ children?: ReactNode; }> & Readonly<Partialize<P, "foo">> & { foo: number; }' is not assignable to type 'P'. [2322]
So basically it鈥檚 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.
It looks like the same problem as in #28884. It fails even if no spread rest.
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.
@ChristianIvicevic
Apparently the type of
props
gets erased as a negative side effect, but you can work around that by casting it back toP
using{...props as P}
when spreading back into the wrapped component.
Is there a bug somewhere where we can track this specific problem?
@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.
@weswigham Is this the same as https://github.com/Microsoft/TypeScript/issues/28748?
Had to use <WrappedComponent {...this.props as any} inject={injected}/>
to temporarily bypass the HOC props merge.
I think that this issue can be closed, since the implicit types of the spread operator were changed.
This is a fully working example you can use with TS 3.5+ (or for 3.2+ use type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
)
type ShadowInjected<T1, T2> = Omit<T1, keyof T2>;
interface HOCProps {
foo : number;
}
export const withSomething = <T,>(WrappedComponent: React.ComponentType<T>): React.FC<ShadowInjected<T, HOCProps>> => {
return function WithSomething(props: ShadowInjected<T, HOCProps>) {
// Do you HOC work here
return (<WrappedComponent foo={1337} {...props as T} />);
};
};
Of course, if you need to shadow only one property explicitly, you don't need the ShadowInjected
type and use Omit<HOCProps, "yourProperty">
instead.
I'm not sure if this is specific to my environment @lvkins , but I was getting 'keyof T2 does not satisfy the constraint keyof T1' which dropped to 'type string is not assignable to type keyof T1'. Fixed by specifying T1 as an extension of T2:
type ShadowInjected<T1 extends T2, T2> = Omit<T1, keyof T2>
Let me know if this is incorrect, am on 3.8.2, been using typescript for a while but sometimes I've troubles grokking the complexities of typing. Thanks!
@lvkins the idea here is to force WrappedComponent
to extend your HOC injected props
my case example
import React, { ComponentType } from 'react'
import { CountrySettingsFlags } from '../country-settings-flags.enum'
import { useCountries } from './countries.hook'
type WithCountriesProps = {
countries: ReturnType<typeof useCountries>
}
function withCountries(flags: CountrySettingsFlags[] = []) {
return function <Props extends WithCountriesProps>(Component: ComponentType<Props>) {
return function (props: Omit<Props, keyof WithCountriesProps>) {
const countries = useCountries(flags)
return <Component {...props as Props} countries={countries} />
// ^^^^^^^^
// without this will also error, what is describing this issue
}
}
}
type TestProps = {
the: number
one: string
}
function Test(props: TestProps) {
return null
}
const Wrapped = withCountries()(Test)
// ^^^^
// error because `TestProps` does not extend `WithCountriesProps`
const test = <Wrapped the={666} one="" />
I'm kind of reviving this thread but casting is not a proper solution, it's highly unsafe. Here's a sample:
import React, { ComponentType } from 'react'
export type BaseProps = {
value: string
name: string
}
export type InjectedProps = {
onChange: () => {}
}
export const withOnChange = <TProps extends BaseProps & InjectedProps>(
Component: ComponentType<TProps>
) => {
return ({ name, ...props }: Omit<TProps, keyof InjectedProps>) => {
return <Component {...(props as TProps)} />
}
}
This sample is totally valid even Component
does not receive either onChange
and name
which are required props. We ship a massive bug due to this in our product today because of this. This really needs to be fixed, casting is such a bad idea and I feel like I have to use it too many times. If someone has a better workaround solution, I would be happy about it
Most helpful comment
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 toP
using{...props as P}
when spreading back into the wrapped component.