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