Variadic tuple types
Spread a generic tuple type inside another tuple to type concat, curry and partial application.
type Arr = readonly any[];
function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U]- Added in TypeScript 4.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$T | type parameter constrained to an array type | A type parameter bounded by an array or tuple type, usually `readonly unknown[]`. Only such a parameter may be spread inside a tuple type as `...T`. |
$...T | generic spread element | A spread of a generic type inside a tuple type. It may appear at the start, middle or end of the tuple, but a tuple may contain only one unbounded rest element and no optional element may follow it. |
Return value
[...T, ...U]A tuple type built by splicing the elements of each spread type in order; if any spread type has unknown length the result becomes unbounded and following elements fold into the rest element type.
Errors
Nothing at runtime — tuples are plain arrays. tsc reports error 1265 for a rest element following another rest element and error 1266 for an optional element following a rest element.
Changelog
- PHP 4.0Introduced: spreads in tuple types may be generic, and rest elements are no longer restricted to the end of a tuple type.
- PHP 4.2Leading and middle rest elements allowed, so `[...string[], number]` and `[boolean, ...string[], boolean]` are legal; a rest element may not be followed by another rest element or by an optional element.
Variadic tuple types made two changes. Spreads inside a tuple type can be generic, so [...T, ...U] is a real type; and rest elements are no longer stuck at the end, so [...Strings, ...Numbers, boolean] and [boolean, ...string[], boolean] are legal.
Why it exists
Before 4.0, concat, curry, zip and compose were typed with towers of overloads — one per arity — and still fell back to any[] past the last one. A single variadic signature covers every arity and keeps element types and positions exact.
The rules that bite
- Spreading a type of unknown length makes the rest of the tuple unbounded:
[...Strings, ...number[], boolean]collapses the tail into a rest element type rather than keepingbooleanin a fixed position. - One rest element per tuple. A rest may not be followed by another rest, nor by an optional element.
- Constrain with
readonly unknown[]rather thanany[]soas constarguments andconsttype parameters still match.
What trips people up
The implementation is not checked against the variadic return type as tightly as you would hope — [...arr1, ...arr2] often needs an assertion inside the function even though every call site is perfectly typed.
Example
When to use it
- Type a concat or zip helper once instead of one overload per arity
- Implement partial application so the returned function has the remaining parameters
- Model a callback whose last argument is fixed, as in [...args: string[], done: boolean]
- Prepend or append an argument to an existing function's parameter list
Frequently asked questions
How do I type a concat function without writing an overload per arity?
Declare both inputs as type parameters constrained to array types and return the spread of both: function concat<T extends readonly unknown[], U extends readonly unknown[]>(a: T, b: U): [...T, ...U]. One signature then covers every arity with exact element types and positions.
Can a tuple type have a rest element that is not last?
Yes, since TypeScript 4.2 — [...string[], number] and [boolean, ...string[], boolean] are both legal. The restrictions are that a tuple may contain only one rest element, and no optional element may follow it.
Why does my tuple type lose its fixed elements after a spread?
Because you spread a type whose length is not known, such as number[]. Once an unbounded spread appears, every element after it is absorbed into the rest element type, so [...Strings, ...number[], boolean] becomes unbounded rather than keeping boolean in a fixed final slot.
More examples
tail, and what unbounded spreads do to the result
r1 keeps each literal in its own slot because the input tuple has a known length. In r2 the spread of myArray has unknown length, so everything after it folds into a rest element and the type becomes [2, 3, 4, ...string[]]. Output is [ 2, 3, 4 ] then [ 2, 3, 4, 'hello', 'world' ] — the runtime arrays are ordinary, only the types differ.
function tail<T extends unknown[]>(arr: readonly [unknown, ...T]): T {
const [, ...rest] = arr;
return rest as T;
}
const myTuple = [1, 2, 3, 4] as const;
const myArray = ["hello", "world"];
const r1 = tail(myTuple);
// const r1: [2, 3, 4]
const r2 = tail([...myTuple, ...myArray] as const);
// const r2: [2, 3, 4, ...string[]]
console.log(r1);
console.log(r2);A fixed trailing argument after variadic ones
The parameter list models a real JavaScript convention — any number of names followed by one flag — which no fixed rest parameter could express. Output is "FEE FI FO", then "fee fi", then "" for the no-names call. Calling doStuff("fee", "fi") without the trailing boolean is a compile error, because the flag is a required element of the tuple.
// Legal since 4.2: the rest element is not last
function doStuff(...args: [...names: string[], shouldCapitalize: boolean]): string {
const shouldCapitalize = args[args.length - 1] as boolean;
const names = args.slice(0, -1) as string[];
return names
.map((n) => (shouldCapitalize ? n.toUpperCase() : n))
.join(" ");
}
console.log(doStuff("fee", "fi", "fo", true));
console.log(doStuff("fee", "fi", false));
console.log(JSON.stringify(doStuff(true)));
Discussion