typescripttypestypescript-genericstype-inference

Why does an extra empty tuple ([]) on generic constraints change the inferred type?


For example:

function foo<T extends number[]>(
  x: T,
  y: (T["length"] extends 2 ? string : never)): T {
  return x;
}

foo([1,2], "test");

This piece of code doesn't compile, since that [1,2] is inferred as number[], not [number, number], and a number[]'s length is not always 2. It makes sense to me.

However:

function bar<T extends [] | number[]>(
  x: T,
  y: (T["length"] extends 2 ? string : never)): T {
  return x;
}

bar([1,2], "test");

Surprisingly, this works! In this case, [1,2] is inferred as [number, number].

But why? Intuitively, T extends number[] and T extends [] | number[] should be 100% equivalent, as [] already extends number[]. However, this redundant [] seems to do something that changes how Typescript compiler infers the generice type. What's the rule behind it?

(A related question showing a use case of this feature: TypeScript: Require that two arrays be the same length?)


Solution

  • TypeScript uses generic constraints as context for inferring generic type arguments.


    For example, a constraint involving string gives TypeScript a hint to infer string literal types instead of just string. Compare the unconstrained

    declare function foo<T>(x: { a: T }): T;
    const x = foo({ a: "y" });
    //    ^? const x: string
    

    to the string-constrained

    declare function foo<T extends string>(x: { a: T }): T;
    const x = foo({ a: "y" });
    //    ^?  const x: "y"
    

    The above isn't particularly well-documented; it is implied by the implementation of microsoft/TypeScript#10676.


    Similarly, a constraint involving tuple types gives TypeScript a hint to infer tuple types instead of just array types. Compare the non-tuple-constrained

    declare function bar<T extends any[]>(x: T): T;
    const z = bar([1, 2]);
    //    ^? const z: number[];
    

    to the tuple-constrained

    declare function bar<T extends any[] | []>(x: T): T;
    const z = bar([1, 2]);
    //    ^? const z: [number, number]
    

    Again, not very well documented. It is described in a comment in microsoft/TypeScript#27179 by the language architect.


    Note that the specifics of this sort of thing tend not to be documented in The TypeScript handbook, as they are even more "advanced" than most of the advanced type information in there. The language lacks a formal specification. Information about more tricky bits of TypeScript tends to be documented in the GitHub issues, if at all. See microsoft/TypeScript#15711 for discussion around documentation. In short, the language is still under active development and the TS team lacks the resources to keep formal documentation up to date.

    Playground link to code