Search code examples
typescripttypescript-typings

Typescript tuple inference in conditional type


In the following snippets, why output1 and output2 are not both inferred as [number, number, number]?

Snippet 1 :

type InferTuple1<T> = T extends any[] ? [...T]: never;

const func1 = <T>(t: InferTuple1<T>) => t;

const output1 = func1([1, 2, 3])

Snippet 2

type InferTuple2<T> = T extends any[] ? [...T]: T;

const func2 = <T>(t: InferTuple2<T>) => t;

const output2 = func2([1, 2, 3]);

output1 is inferred as:

const output1: [number, number, number]

output2 is inferred as:

const output2: number[]
  • The type inference in the first case is clear to me : the input is an array (so it extends any[]) then it is inferred as [...T], which converts T into a tuple of type [number, number, number]

  • In the second case, the input is still an array, but it seems that it is the second conditional branch which is executed, I don't understand why (I would expect Typescript to return the first one, since the condition is verified).

(in the same fashion func1(["foo", 42, "bar"]) is inferred as [string, number, string], whereas func2(["foo", 42, "bar"]) is inferred as (string | number)[])


Solution

  • In both cases the conditional type, when evaluated, selects the true branch, because T extends any[] is true. The difference is just how TypeScript infers the generic type argument T when you call func1() or func2(). In both cases the argument is [1, 2, 3]. The compiler is technically free to infer anything compatible with that argument. For func1() it infers the tuple type [number, number, number], while for func2() it infers the unordered arbitrary-length array type number[]. Neither one is "incorrect", but different heuristics come into play that change the preference.

    The details of exactly how such inference works aren't completely documented (except for walking through the compiler code) so any explanation I give here is going to be, at best, illustrative of the kind of thing going on, and not necessarily completely accurate.


    TypeScript 4.0 introduced variadic tuple types as implemented in microsoft/TypeScript#39094. According to that pull request:

    The type [...T], where T is an array-like type parameter, can conveniently be used to indicate a preference for inference of tuple types.

    So if you write <T extends any[]>(t: [...T]) => t you will tend to get tuples, whereas if you write <T extends any[]>(t: T) => t you will tend to get arrays.

    So with <T,>(t: T extends any[] ? [...T] : never) => t, the variadic tuple type shows a preference for tuples. But apparently with <T,>(t: T extends any[] ? [...T] : T) => t it does not, and the T being present interferes with the inference. Exactly how it interferes is where my knowledge peters out. I'd guess that T is a "stronger" inference candidate than [...T] in the conditional type, and so [...T] is ignored for inference there. This can't be the whole story, since this "strength" doesn't stop <T extends any[]>(t: T | [...T]) => t from preferring tuples, and T extends any[] ? [...T]: T is a subtype of T | [...T].


    So there you go. If I ever find out more specific details about how inference of variadic tuples through conditional types work then I'll come back and edit those in here.

    My only solid take-away here is that you should try not to play games with variadic tuples and conditional types if you care about inference. Inferring tuples is probably more easily accomplished with const type parameters, such as <const T,>(t: T) => t. But that's out of scope for the question as asked, and might or might not be appropriate for your use cases.

    Playground link to code