Search code examples
typescripttuplescovarianceunion-typessubtyping

Why is [1 | 2] not a subtype of [1 | []] | [2 | []] in TypeScript?


In TypeScript, I have an API whose input is a union of tuples and I have a value whose type is a tuple of unions that I want to pass into it. It seems like this works in most cases, but I can't figure out why it doesn't work in my case, and what I can do to get it to work without needing a type assertion. Here is a reproduction of the issue (TS Playground link):

(x: [1 | 2]): [1] | [2]  => x; // OK
(x: [1 | 2]): [1 | 11] | [2 | 22]  => x; // OK
(x: [1 | 2]): [1 | []] | [2 | 22]  => x; // OK
(x: [1 | 2]): [1 | []] | [2 | []]  => x; // ERROR

Even though the first three examples are okay, the fourth example gives the error:

Type '[1 | 2]' is not assignable to type '[1 | []] | [2 | []]'.
  Type '[1 | 2]' is not assignable to type '[1 | []]'.
    Type '1 | 2' is not assignable to type '1 | []'.
      Type '2' is not assignable to type '1 | []'.

Solution

  • Generally speaking, TypeScript does not automatically distribute all covariant type operators over union types. So {a: X | Y} does not become {a: X} | {a: Y}, and [X | Y] does not become [X] | [Y]. Doing something like this in general would cause an explosion of union types (imagine what [X | Y, T | U | V | W, Q | R | S] would become). It also doesn't automatically detect if two types would be compatible if each were distributed that way. It would be a lot of work for the compiler to do, for limited benefit. Before TypeScript 3.5, all of your examples were errors.

    TypeScript 3.5 introduced so-called "smarter" union type checking, as implemented in microsoft/TypeScript#30779, in which the above distribution does occur, but only if the union types in question are discriminated unions, and only if the unions being examined would have 25 or fewer members. These restrictions put a limit on how much extra work TypeScript needs to expend to check these things, but it also results in behavior that developers think might be inconsistent or arbitrary.

    So, examining

    (x: [1 | 2]): [1] | [2] => x; // OK
    (x: [1 | 2]): [1 | 11] | [2 | 22] => x; // OK
    (x: [1 | 2]): [1 | []] | [2 | 22] => x; // OK
    (x: [1 | 2]): [1 | []] | [2 | []] => x; // ERROR
    

    In the first three cases, the union types [1] | [2], [1 | 11] | [2 | 22], and [1 | []] | [2 | 22] are all considered to be discriminated unions. The tuple element (the property at index "0") is a literal type or union of literal types in at least one member of the union. But the type [1 | []] | [2 | []] is not considered to be a discriminated union. The empty tuple type [] is not considered a literal type, and since it exists in each member of the union and no member of the union is just composed of literal types, it fails to meet the criteria.

    And that means the "smarter" union type checking occurs in the first three examples but does not occur in the last example.