You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Incorrect assignability check when using Extract (TS2322) #46413
This is the behavior in every version I tried, and I reviewed the FAQ for entries about Generics, Assignability, and Extract.
Tested with 4.2.3, 4.3.5, 4.4.4, and 4.5.0-dev.20211018.
In 4.1.5, the example using a type alias also fails. Version 4.2 introduced Smarter Type Alias Preservation, which may explain why that example passes starting from 4.2.x.
I have attempted to minimize this example as much as possible. The use of Extract to define a field type appears to be significant.
typeA<T>={val: Extract<number|string,T>;};typeA_number=A<number>;functionf1(x: A_number): A<number|string>{// Passes type checking.returnx;}functionf2(x: A<number>): A<number|string>{// Fails type checking with error TS2322:// Type 'A<number>' is not assignable to type 'A<string | number>'.// Type 'string | number' is not assignable to type 'number'.// Type 'string' is not assignable to type 'number'.returnx;}
🙁 Actual behavior
The type checker reports error TS2322: A<number> is not assignable to type A<string | number> because string | number is not assignable to number. It appears to be checking assignability of a field in the wrong direction, because the assignability check should be whether number is assignable to string | number.
Using a type alias for A<number> does not produce the same error. I was also not able to reproduce this error without using Extract to define a field type.
🙂 Expected behavior
The type A<number> should be assignable to type A<string | number> because it is a simple object type and number is assignable to string | number.
A<T> is contravariant in T because it’s subtractingT from another type rather than using it as-is. A wider type for T will therefore produce a narrower A<T>, so the error is expected.
A<T> is contravariant in T because it’s subtractingT from another type rather than using it as-is. A wider type for T will therefore produce a narrower A<T>, so the error is expected.
I don't see where it's subtracting T from another type, and thus contravariant. Extract<T, U> is defined as "Extract those types in T that are assignable to U." If a type S in T is assignable to U, then S is also assignable to U | V by definition. In the case of A<T>, I don't see how widening T would narrow the type of A<T>. Performing the type substitution should make this clear:
Sorry, my mistake, I got Extract mixed up with Exclude. That said, the issue/bug appears to be that TS is measuringA<T> as contravariant for some reason:
Type 'A<number>' is not assignable to type 'A<string | number>'.
Type 'string | number' is not assignable to type 'number'.
string | number indeed isn't assignable to number, but this is going in the opposite direction relative to A. So for whatever reason it seems to think T is contravariant. 🤔
Type 'A<string | number>' is not assignable to type 'A<number>'.
Types of property 'val' are incompatible.
Type 'string | number' is not assignable to type 'number'.
Type 'string' is not assignable to type 'number'.
This makes sense for general conditional types (consider type X<T> = {} extends T ? 1 : 2; X<{}> isn't assignable to or from X<{ x: string }>); Extract is just a special case where it's only covariant.
The real bug here afaict is that TS is doing a variance check here instead of doing structural comparison; the variance measure should probably be tagged with VarianceFlags.Unreliable (which means that variance checking may produce false negatives, but not false positives).
Activity
fatcerberus commentedon Oct 18, 2021
A<T>is contravariant inTbecause it’s subtractingTfrom another type rather than using it as-is. A wider type forTwill therefore produce a narrowerA<T>, so the error is expected.tonygoold commentedon Oct 18, 2021
I don't see where it's subtracting
Tfrom another type, and thus contravariant.Extract<T, U>is defined as "Extract those types in T that are assignable to U." If a typeSinTis assignable toU, thenSis also assignable toU | Vby definition. In the case ofA<T>, I don't see how wideningTwould narrow the type ofA<T>. Performing the type substitution should make this clear:At worst,
A<T | V>is equal toA<T>(e.g., where neither number nor string is assignable toV).fatcerberus commentedon Oct 18, 2021
Sorry, my mistake, I got
Extractmixed up withExclude. That said, the issue/bug appears to be that TS is measuringA<T>as contravariant for some reason:string | numberindeed isn't assignable tonumber, but this is going in the opposite direction relative toA. So for whatever reason it seems to thinkTis contravariant. 🤔tjjfvi commentedon Oct 18, 2021
It thinks that
Ais invariant; this also fails:This makes sense for general conditional types (consider
type X<T> = {} extends T ? 1 : 2;X<{}>isn't assignable to or fromX<{ x: string }>);Extractis just a special case where it's only covariant.The real bug here afaict is that TS is doing a variance check here instead of doing structural comparison; the variance measure should probably be tagged with
VarianceFlags.Unreliable(which means that variance checking may produce false negatives, but not false positives).tjjfvi commentedon Oct 18, 2021
Possibly related: #43887