Skip to content

Incorrect assignability check when using Extract (TS2322) #46413

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
tonygoold opened this issue Oct 18, 2021 · 5 comments
Open

Incorrect assignability check when using Extract (TS2322) #46413

tonygoold opened this issue Oct 18, 2021 · 5 comments
Labels
Bug A bug in TypeScript
Milestone

Comments

@tonygoold
Copy link

Bug Report

🔎 Search Terms

extract assignable TS2322

🕗 Version & Regression Information

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.

⏯ Playground Link

Playground link with relevant code

💻 Code

I have attempted to minimize this example as much as possible. The use of Extract to define a field type appears to be significant.

type A<T> = {
    val: Extract<number | string, T>;
};
type A_number = A<number>;

function f1(x: A_number): A<number| string> {
    // Passes type checking.
    return x;
}

function f2(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'.
    return x;
}

🙁 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.

@fatcerberus
Copy link

fatcerberus commented Oct 18, 2021

A<T> is contravariant in T because it’s subtracting T 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.

@tonygoold
Copy link
Author

tonygoold commented Oct 18, 2021

A<T> is contravariant in T because it’s subtracting T 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:

type A<T> = {
    val: Extract<number | string, T>;
};
type An = A<number>; /*
        = { val: Extract<number | string, number>; };
        = { val: number; }; */
type An = A<number | string>; /*
        = { val: Extract<number | string, number | string>; };
        = { val: number | string; }; */

At worst, A<T | V> is equal to A<T> (e.g., where neither number nor string is assignable to V).

@fatcerberus
Copy link

fatcerberus commented Oct 18, 2021

Sorry, my mistake, I got Extract mixed up with Exclude. That said, the issue/bug appears to be that TS is measuring A<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. 🤔

@tjjfvi
Copy link
Contributor

tjjfvi commented Oct 18, 2021

It thinks that A is invariant; this also fails:

function f2(x: A<number | string>): A<number> {
    return x;
}
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).

@tjjfvi
Copy link
Contributor

tjjfvi commented Oct 18, 2021

Possibly related: #43887

@andrewbranch andrewbranch added the Bug A bug in TypeScript label Nov 12, 2021
@andrewbranch andrewbranch added this to the Backlog milestone Nov 12, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants