Variadic tuple types

Spread a generic tuple type inside another tuple to type concat, curry and partial application.

Signature
type Arr = readonly any[];

function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U]
  • Added in TypeScript 4.0

Parameters

ParameterTypeDescription
$Ttype parameter constrained to an array typeA 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`.
$...Tgeneric spread elementA 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 keeping boolean in 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 than any[] so as const arguments and const type 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · typescript
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.

Example · typescript
// 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

  • Be the first to comment on this lesson.