TypeScript Version: 3.7.2, also tested in nightly (3.8.0-dev.20191224)
Search Terms:
"is assignable to the constraint of type", "could be instantiated with a different subtype of constraint"
Code
function test<T extends {accepted: boolean}>(cb: (value: T) => void) {
return (data: Omit<T, 'accepted'>) => cb({...data, accepted: true});
}
Expected behavior:
No error
Actual behavior:
Argument of type 'Pick<T, Exclude<keyof T, "accepted">> & { accepted: true; }' is not assignable to parameter of type 'T'.
'Pick<T, Exclude<keyof T, "accepted">> & { accepted: true; }' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{ accepted: boolean; }'.
Related Issues:
T
could be instantiated with { accepted: false; }
, couldn't it?
T
could be instantiated with{ accepted: false; }
, couldn't it?
I believe that's not the reason, changing accepted: boolean
to accepted: true
still gives the same error: http://www.typescriptlang.org/play/?ts=3.8.0-dev.20191224&ssl=4&ssc=1&pln=1&pc=1#code/GYVwdgxgLglg9mABFApgZygHgCqJQD1TABM1EBvAQwghQAdViAuZAJxBQF8A+ACggBGLXgDdKAGw4tsASkQBebohFwYxOeQBQiRKxRQQrJL2KUolFgHkAtjCzYANIgDk1WgxTFn3OYsSDecgA6ENNzJzd6RhYodi4ZAG5NTk0gA
Even if it was the reason, this would be incorrect from types point of view:
cb: (v: T) => void
, T extends { accepted: boolean }
-> we should be able to call cb
with argument of type T & {accepted: true}
TS isn't capable of the higher-order reasoning needed to understand that Exclude<T, k> & { [k]: T[k] }
is equivalent to T
. You can only make that determination through understanding what Exclude
and &
actually do at a higher level, but from TS's point of view, Exclude
is just a type alias
I find this usage of Omit
to be really useful for HOCs, and found it unintuitive that Typescript runs into this design limitation.
In this example, is there any better approach than just doing a type assertion, e.g.,
function test<T extends {accepted: boolean}>(cb: (value: T) => void) {
return (data: Omit<T, 'accepted'>) => {
const newData = {...data, accepted: true} as T;
return cb(newData);
}
}
Is this here the same problem?
type MyType = {
somethingFromHoc: string;
}
type DerivedType<T extends {}, U extends T> = Omit<U, keyof T>;
const anotherThing = <P extends MyType>(input: DerivedType<MyType, P>): P =>{
return {
...input,
somethingFromHoc: "hello"
};j
}
https://www.typescriptlang.org/play/?ssl=15&ssc=1&pln=1&pc=1#code/C4TwDgpgBAsiAq5oF4oG8BQUoGcD2AthMABYCWAdgOYBiAToQBJ4DGAXLsHZVQNxQYAvhgyhIUACIRuANwgATRJAA88KBAAewCBXk50ggDRQAquq069UeAD4oqAPIEywZSeMBrCCDwAzazb8Iix4FDjAUACGFHik0vDk1PZQygAK5tq6+nBKEDYAFJRgAK7AHFKyCrnKOUjGqTYAlBzpyDaY2HTExXQU6FjYUAB0I0WlhgPY+ESkPPRMrBwARCQQADZreEsDgrwAVgLCIkA
Is easily solved with an as P
assertion.
I would like to expose a simpler example for which an unexpected error is emitted too, that perhaps helps in tracking down the issue here:
/*
Type '"foo"' is not assignable to type 'T'. '"foo"' is assignable to the constraint
of type 'T', but 'T' could be instantiated with a different subtype of constraint
'"foo" | "bar"'.
*/
function f<T extends 'foo' | 'bar'>(t: T = 'foo') {}
@inad9300
I'm pretty sure you want just:
function f(t: 'foo' | 'bar' = 'foo') {}
Otherwise what happens if T
is instantiated with 'bar'
?
function f<'bar'>(t: 'bar' = 'foo')
The error message is super accurate here.
Inlining the type as in your first snippet would not allow type reuse, unfortunately.
Perhaps what should happen is, an error should be issued when the conflicting situation occurs, and not before. With the overall goal of avoiding having to provide generics explicitly as much as possible, I'd be typically writing things like f()
or f('bar')
. In this case, TypeScript reporting this error before it actually happens hinders type inference, which is one of the best features of the language.
There's another solution if you don't want to use as
to casting
Thanks for my workmate馃槏
function test<T>(cb: (value: T & { accepted: boolean }) => void) {
return (data: T) => cb({...data, accepted: true});
}
This confused me for a long time.
I expected this should work, but it did not.
But this did.
See how the only difference is the extracted props. After messing around for a long time I suspected it was the extracted props, then saw @dwjohnston's post on SO that pointed me here. Rough!
Is this here the same problem?
type MyType = { somethingFromHoc: string; } type DerivedType<T extends {}, U extends T> = Omit<U, keyof T>; const anotherThing = <P extends MyType>(input: DerivedType<MyType, P>): P =>{ return { ...input, somethingFromHoc: "hello" };j }
https://www.typescriptlang.org/play/?ssl=15&ssc=1&pln=1&pc=1#code/C4TwDgpgBAsiAq5oF4oG8BQUoGcD2AthMABYCWAdgOYBiAToQBJ4DGAXLsHZVQNxQYAvhgyhIUACIRuANwgATRJAA88KBAAewCBXk50ggDRQAquq069UeAD4oqAPIEywZSeMBrCCDwAzazb8Iix4FDjAUACGFHik0vDk1PZQygAK5tq6+nBKEDYAFJRgAK7AHFKyCrnKOUjGqTYAlBzpyDaY2HTExXQU6FjYUAB0I0WlhgPY+ESkPPRMrBwARCQQADZreEsDgrwAVgLCIkA
Is easily solved with an
as P
assertion.
@dwjohnston What about this approach?
@RyanCavanaugh Could you please if this is an acceptable way?
function withOwner(owner: string) {
return function <T extends { owner: string }>(
Component: React.ComponentType<T>
): React.ComponentType<Omit<T, "owner"> & {owner?: never}> {
return function (props) {
const newProps = { ...props, owner };
return <Component {...newProps} />;
};
};
}
https://www.typescriptlang.org/play?strictFunctionTypes=false&jsx=1#code/JYWwDg9gTgLgBAJQKYEMDG8BmUIjgIilQ3wG4AoczAVwDsNgJa4B3YGACwHkXakoAFBF78AXHADOMKMFoBzAJRwA3uThwiMalGY16MRswA8AFThIAHjCS0AJhJVxhfKOKkz5cAL4A+AWvU4AGFcSD5aGHFkdBgAOhDwJhsYEwBPMCRTHwCFKOI4hLDktIyjLhB2UwAaAmd+fB84ADIVOqgAfnE+ADd+XxUA9U1tXToGJjgBMBwwCSVVQMC0Jik4PhYABRmHAF5HWIPpiFmatu8KRaGkLR04I0KkiJUD2PWt44kvOAB6HwvArz-QHkLyUGDpJDBFAwd6zOB7BZwAA2wF6Ei61BAACN+P82m5pLI5BRgXpxswgtCBMpkaikBJTiIoN5xJSYdt5gFhrd-IsjLZUdlLip8ARQcKALJIYTiZQotFeGo8FyytriwJGb4C7pCuAKEmUZa0VbKpC2Nnw1jsbhMgT4CQsVIWfAKARs-WUe7Q2lonbKACcXzaO3tjudPz+P2+cE4wAcEg4EGoSNscBxcEwsiQ5DKInN3vl9L9gacTJDDqdot+pCjMY4cckieTqY4KF6cBQMYhAFEoDgoDnTfn4IWJMWvtXa7H402U2nIZm+JRyOCMnAACIQOSwhyI2goEBIAkeOQBfGSQnyEFUMYGCabuTU-eHxkuLziB87zlXG7GbWNAB1CAIEwWVnyQRU4FNVxWiZLxNX-a8jRNPMH0tNhOGgu0K2dV0Hw9T00PAkNM1sCBRWDUNKwjGtvmjadGyTOd00XbNcz4WwiIPJASOAMiKLLKjw0nOi6wbBMmJbNtIU7VckF7ftB1Qrc1m43j+Joqd6xnST5wzLMgA
Most helpful comment
There's another solution if you don't want to use
as
to castingThanks for my workmate馃槏
Playground Link