Variadic tuple types by ahejlsberg · Pull Request #39094 · microsoft/TypeScript (original) (raw)

This PR implements variadic tuple types, i.e. the ability for tuple types to have spreads of generic types that can be replaced with actual elements through type instantiation. The PR effectively implements the features discussed in #5453.

Some examples of new capabilities provided in this PR:

// Variadic tuple elements

type Foo<T extends unknown[]> = [string, ...T, number];

type T1 = Foo<[boolean]>; // [string, boolean, number] type T2 = Foo<[number, number]>; // [string, number, number, number] type T3 = Foo<[]>; // [string, number]

// Strongly typed tuple concatenation

function concat<T extends unknown[], U extends unknown[]>(t: [...T], u: [...U]): [...T, ...U] { return [...t, ...u]; }

const ns = [0, 1, 2, 3]; // number[]

const t1 = concat([1, 2], ['hello']); // [number, number, string] const t2 = concat([true], t1); // [boolean, number, number, string] const t3 = concat([true], ns); // [boolean, ...number[]]

// Inferring parts of tuple types

declare function foo<T extends string[], U>(...args: [...T, () => void]): T;

foo(() => {}); // [] foo('hello', 'world', () => {}); // ["hello", "world"] foo('hello', 42, () => {}); // Error, number not assignable to string

// Inferring to a composite tuple type

function curry<T extends unknown[], U extends unknown[], R>(f: (...args: [...T, ...U]) => R, ...a: T) { return (...b: U) => f(...a, ...b); }

const fn1 = (a: number, b: string, c: boolean, d: string[]) => 0;

const c0 = curry(fn1); // (a: number, b: string, c: boolean, d: string[]) => number const c1 = curry(fn1, 1); // (b: string, c: boolean, d: string[]) => number const c2 = curry(fn1, 1, 'abc'); // (c: boolean, d: string[]) => number const c3 = curry(fn1, 1, 'abc', true); // (d: string[]) => number const c4 = curry(fn1, 1, 'abc', true, ['x', 'y']); // () => number

Structure and instantiation

The basic structure of a non-generic tuple type remains unchanged with this PR: Zero or more required elements, followed by zero or more optional elements, optionally followed by a rest element (for example [A, B?, ...C[]]). However, it is now possible to have variadic elements anywhere in a tuple type, except following the optional rest element (for example [A, ...T, B?, ...U, ...C[]]).

A variadic elemement is a spread element of the form ...T, where T is a generic type constrained to any array or tuple type (specifically, any type that is assignable to readonly any[]). Intuitively, a variadic element ...T is a placeholder that is replaced with one or more elements through generic type instantiation. Instantiation of a tuple type with a variadic element ...T depends on the type argument provided for T as follows:

Instantiation of a generic tuple type includes normalization to ensure the resulting tuple type follows the basic structure described above. Specifically:

NOTE: With #41544 we now support starting and middle rest elements in tuple types.

Type relationships

Generally, a tuple type S is related to a tuple type T by pairwise relating elements of S to the elements of T. Variadic elements are processed as follows:

Some examples:

function foo1<T extends unknown[], U extends T>(x: [string, ...unknown[]], y: [string, ...T], z: [string, ...U]) { x = y; // Ok x = z; // Ok y = x; // Error y = z; // Ok z = x; // Error z = y; // Error }

Tuple types with single variadic elements have the following relations:

Some examples:

function foo2<T extends readonly unknown[]>(t: T, m: [...T], r: readonly [...T]) { t = m; // Ok t = r; // Error m = t; // Error m = r; // Error r = t; // Ok r = m; // Ok }

Type inference

Inference between tuple types with the same structure (i.e. same number of elements and fixed, variadic, or rest kind matched to the same kind in each position), simply infers pairwise between the element types. For example, inference from [string, ...Partial<S>, number?] to [string, ...T, number?] infers Partial<S> for T.

Inference between tuple types S and T with different structure divides each tuple into a starting fixed part, a middle part, and an ending fixed part. Any one of these parts may be empty.

Inference then proceeds as follows:

In the context of inference for a call of a generic function with a rest parameter R, the implied arity for R is the number of rest arguments supplied for R. In all other contexts, a type parameter has no implied arity. For an example of inference involving an implied arity, see the curry function in the introduction.

Some examples:

type First<T extends readonly unknown[]> = T[0]; type DropFirst<T extends readonly unknown[]> = T extends readonly [any?, ...infer U] ? U : [...T]; type Last<T extends readonly unknown[]> = T extends readonly [...infer _, infer U] ? U : T extends readonly [...infer _, (infer U)?] ? U | undefined : undefined; type DropLast<T extends readonly unknown[]> = T extends readonly [...infer U, any?] ? U : [...T];

type T1 = First<[number, boolean, string]>; // [number] type T2 = DropFirst<[number, boolean, string]>; // [boolean, string] type T3 = Last<[number, boolean, string]>; // [string] type T4 = DropLast<[number, boolean, string]>; // [number, boolean]

Spreads in array literals

When an array literal has a tuple type, a spread of a value of a generic array-like type produces a variadic element. For example:

function foo3<T extends unknown[], U extends unknown[]>(t: [...T], u: [...U]) { return [1, ...t, 2, ...u, 3] as const; // readonly [1, ...T, 2, ...U, 3] }

const t = foo3(['hello'], [10, true]); // readonly [1, string, 2, number, boolean, 3]

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

declare function ft1<T extends unknown[]>(t: T): T; declare function ft2<T extends unknown[]>(t: T): readonly [...T]; declare function ft3<T extends unknown[]>(t: [...T]): T; declare function ft4<T extends unknown[]>(t: [...T]): readonly [...T];

ft1(['hello', 42]); // (string | number)[] ft2(['hello', 42]); // readonly (string | number)[] ft3(['hello', 42]); // [string, number] ft4(['hello', 42]); // readonly [string, number]

Indexing and destructuring

Indexing and destructuring of generic tuple types appropriately recognizes fixed elements at the start of the tuple type. Beyond the fixed elements, the type is simply a union of the remaining element types.

function f1<T extends unknown[]>(t: [string, ...T], n: number) { const a = t[0]; // string const b = t[1]; // [string, ...T][1] const c = t[2]; // [string, ...T][2] const d = t[n]; // [string, ...T][number] }

function f2<T extends unknown[]>(t: [string, ...T, number], n: number) { const a = t[0]; // string const b = t[1]; // [string, ...T, number][1] const c = t[2]; // [string, ...T, number][2] const d = t[n]; // [string, ...T, number][number] }

function f3<T extends unknown[]>(t: [string, ...T]) { let [...ax] = t; // [string, ...T] let [b1, ...bx] = t; // string, [...T] let [c1, c2, ...cx] = t; // string, [string, ...T][1], T[number][] }

function f4<T extends unknown[]>(t: [string, ...T, number]) { let [...ax] = t; // [string, ...T, number] let [b1, ...bx] = t; // string, [...T, number] let [c1, c2, ...cx] = t; // string, [string, ...T, number][1], (number | T[number])[] }

Rest parameters and spread arguments

Spread expressions with fixed length tuples are now appropriately flattened in argument lists. For example:

declare function fs1(a: number, b: string, c: boolean, ...d: number[]): void;

function fs2(t1: [number, string], t2: [boolean], a1: number[]) { fs1(1, 'abc', true, 42, 43, 44); fs1(...t1, true, 42, 43, 44); fs1(...t1, ...t2, 42, 43, 44); fs1(...t1, ...t2, ...a1); fs1(...t1); // Error: Expected at least 3 arguments, but got 2 fs1(...t1, 45); // Error: Type '45' is not assignable to type 'boolean' }

A rest parameter of a generic tuple type can be used to infer types from the middle part of argument lists. For example:

declare function fr1<T extends unknown[]>(x: number, ...args: [...T, number]): T;

function fr2<U extends unknown[]>(u: U) { fr1(1, 2); // [] fr1(1, 'hello', true, 2); // [string, boolean] fr1(1, ...u, 'hi', 2); // [...U, string] fr1(1); // Error: Expected 2 arguments, but got 1 }

Application of mapped types

When a mapped type is applied to a generic tuple type, non-variadic elements are eagerly mapped but variadic elements continue to be generic. Effectively, M<[A, B?, ...T, ...C[]] is resolved as [...M<[A]>, ...M<[B?]>, ...M<T>, ...M<C[]>]. For example:

type TP1<T extends unknown[]> = Partial<[string, ...T, number]>; // [string?, ...Partial, number?]

Fixes #5453.
Fixes #26113.