The error:
'T' could be instantiated with an arbitrary type which could be unrelated to '{ value: string; onChange: Dispatch<SetStateAction<string>>; }'.ts(2322)
is correct.
Consider this example:
interface ControlledInput {
value: string;
onChange: (newValue: string) => void;
}
interface FooProps {
value: string;
onChange: (newValue: string) => void;
name: "hello";
}
const Foo: FC<FooProps> = () => null;
const withControl = <T extends ControlledInput>(Elem: FC<T>) => () =>
// props: Omit<T, keyof ControlledInput>
{
const [value, setValue] = React.useState("");
return <Elem value={value} onChange={setValue} />;
};
withControl(Foo);
FooProps
is a subtype of ControlledInput
. This line withControl(Foo)
compiles. But you ended up in a situation where Foo
has required prop name
whereas you have provided only value
and onChange
.
In order to fix it, just get rid of generic:
interface ControlledInput {
value: string;
onChange: (newValue: string) => void;
}
interface FooProps {
value: string;
onChange: (newValue: string) => void;
name: "hello";
}
const Foo: FC<FooProps> = () => null;
const withControl = (Elem: FC<ControlledInput>) => () =>
// props: Omit<T, keyof ControlledInput>
{
const [value, setValue] = React.useState("");
return <Elem value={value} onChange={setValue} />;
};
withControl(Foo); // expected error
SAFE FIX
could you please provide an example, where prop types of component passed as argument carry over to resulting component
You can use this pattern:
import React, { FC } from 'react'
interface MainProps {
value: string;
onChange: (value: string) => void
name: string;
}
type ControlProps = {
children: (value: string, setValue: (value: string) => void) => JSX.Element
}
const Control: FC<ControlProps> = ({ children }) => {
const [value, setValue] = React.useState('');
return children(value, setValue)
}
const Foo = (props: MainProps) => <div></div>;
const App =() => {
<Control>{
(value, onChange) =>
<Foo value={value} onChange={onChange} name='hello' />
}
</Control>
}
Playground
UNSAFE FIX
If you still want to stick with your approach, you can turn off this flag:strictFunctionTypes
.
In general it is unsafe what you are trying to do. Consider this example:
You can override value & onChange
in T
and intersect it back with ControlledInput
, like here:
import React, { FC } from 'react'
type ControlledInput = {
value: string;
onChange: (newValue: string) => void;
}
interface FooProps {
value: string;
onChange: (newValue: string) => void;
}
const Foo: FC<FooProps> = (props) => null;
type Keys = 'value' | 'onChange';
const withControl = <T extends ControlledInput>(
Elem: FC<Omit<T, Keys> & ControlledInput>
) =>
<Props extends Omit<T, keyof ControlledInput>>(props: Props) => {
const [value, setValue] = React.useState("");
return <Elem value={value} onChange={setValue} {...props} />;
};
withControl(Foo)({ name: 'hellow' }); // ok
This code compiles, but once you add extra property to Foo
, it will fail. Because TypeScript checks arguments from left to right. In this case, we ended up with similar error: FooProps
type is assignable to ControlledInput
but not vice versa because FooProps
is a subtype.
If you want to make it work, you should use explicit generic argument. I'm not a huge fan of this technique but is seems that it is the only reasonable way to solve this issue:
type ControlledInput = {
value: string;
onChange: (newValue: string) => void;
}
interface FooProps {
value: string;
onChange: (newValue: string) => void;
name: string;
}
const Foo: FC<FooProps> = (props) => null;
type Keys = 'value' | 'onChange';
const withControl = <T extends ControlledInput>(
Elem: FC<Omit<T, Keys> & ControlledInput>
) =>
<Props extends Omit<T, keyof ControlledInput>>(props: Props) => {
const [value, setValue] = React.useState("");
return <Elem value={value} onChange={setValue} {...props} />;
};
withControl<FooProps>(Foo)({ name: 'hello' }); // ok