TypeScript Tuple Types in Depth
Tuple surgery builds new tuple types from old ones with variadic spreads and infer. Each build is proven by the values its positions accept.
Concat spreads two tuples into one
Concat spreads the first tuple and then the second into one tuple type. The value shows all three positions in order.
type Concat<A extends readonly unknown[], B extends readonly unknown[]> = [...A, ...B];
type Combined = Concat<[10, 20], ['thirty']>;
const combined: Combined = [10, 20, 'thirty'];
console.log(combined[0]);
console.log(combined[1]);
console.log(combined[2]);Push appends one member at the end
Push spreads the input tuple and adds one member type after it. The last position and the length prove the append.
type Push<T extends readonly unknown[], V> = [...T, V];
type WithEnd = Push<[10, 20], 'end'>;
const values: WithEnd = [10, 20, 'end'];
console.log(values[2]);
console.log(values.length);Reverse rebuilds a tuple back to front
Reverse binds the head and the rest with infer, reverses the rest, and places the head last. The value reads in the reversed order.
type Reverse<T extends readonly unknown[]> = T extends readonly [infer Head, ...infer Rest] ? [...Reverse<Rest>, Head] : [];
type Reversed = Reverse<[1, 'two', true]>;
const reversed: Reversed = [true, 'two', 1];
console.log(reversed[0]);
console.log(reversed[1]);
console.log(reversed[2]);Head and Tail read a tuple apart
Head binds the first member and Tail binds the remaining tuple. Two assignments prove the split.
type Head<T extends readonly unknown[]> = T extends readonly [infer H, ...unknown[]] ? H : never;
type Tail<T extends readonly unknown[]> = T extends readonly [unknown, ...infer Rest] ? Rest : [];
const headValue: Head<[9, 8, 7]> = 9;
const tailValues: Tail<[9, 8, 7]> = [8, 7];
console.log(headValue);
console.log(tailValues[0]);
console.log(tailValues[1]);A wrong member type is refused
The built tuple ends with the literal end in its last position. A number in that position is refused.
type Push<T extends readonly unknown[], V> = [...T, V];
type WithEnd = Push<[10, 20], 'end'>;
const values: WithEnd = [10, 20, 30];
console.log(values);Example
type Concat<A extends readonly unknown[], B extends readonly unknown[]> = [...A, ...B];
type Push<T extends readonly unknown[], V> = [...T, V];
type Reverse<T extends readonly unknown[]> = T extends readonly [infer Head, ...infer Rest] ? [...Reverse<Rest>, Head] : [];
type Head<T extends readonly unknown[]> = T extends readonly [infer H, ...unknown[]] ? H : never;
type Base = [1, 2];
type Extended = Push<Base, 3>;
type Joined = Concat<Extended, ['done']>;
type Flipped = Reverse<Joined>;
const flipped: Flipped = ['done', 3, 2, 1];
const headValue: Head<Flipped> = 'done';
console.log(headValue);
console.log(flipped[1]);
console.log(flipped[2]);
console.log(flipped[3]);Key points
- A variadic tuple type spreads one tuple inside another, so Concat joins two tuples position by position.
- Push appends one element type at the end by spreading the input tuple before the new member.
- Reverse is recursive, and each step moves the head to the end of the reversed rest.
- infer in a tuple pattern binds the head and the rest, so Head and Tail read a tuple apart.
- A position in the built tuple accepts only the type the surgery placed there.
In this example, Push extends a base tuple, Concat joins a final member, Reverse flips the joined tuple, and Head reads the new first member off the result.
Note
The builds match readonly inputs with readonly patterns, so plain and readonly tuples both work.
Try it Yourself
Edit the code, then press Run.
Running is simulated ยท TypeScript Playground
Exercise
What does Push<[1, 2], 3> evaluate to as a type?