Variadic Tuple Types

Reviewed & published by Brayan K

A variadic tuple type places a spread element inside a tuple — like [first, ...rest] or [...T, last] — letting its length and element types vary with a type parameter so functions such as concat and curry stay precisely typed.

Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

Think of a train. A plain array is "some carriages" — you've forgotten which is the engine and which is the caboose. A variadic tuple is the precise consist: "engine, then these middle carriages, then the caboose." [first, ...rest] says "engine plus whatever follows"; [...rest, last] says "whatever comes before, then the caboose." Every carriage keeps its exact role and position, no matter how many sit in the middle.

1. Spreads Inside Tuple Types

A spread element inside a tuple type makes it variadic: [id: number, ...rest: T] is "a number, then however many elements T describes." The spread can sit at the start, middle, or end, mirroring how the array spread operator works at runtime — though a tuple type may contain at most one rest element.

// A spread element inside a tuple type makes it VARIADIC — its length/contents
// depend on a type parameter:
type WithId<T extends unknown[]> = [id: number, ...rest: T];

type A = WithId<[string, boolean]>; // [id: number, string, boolean]
type B = WithId<[]>;                // [id: number]

// The spread can sit at the start, middle, or end:
type StartsWith<H, T extends unknown[]> = [H, ...T];
type EndsWith<T extends unknown[], L> = [...T, L];

// Only ONE rest element is allowed per tuple type:
// type Bad = [...string[], ...number[]]; // ❌ a tuple can have at most one rest
// A VARIADIC TUPLE TYPE uses a spread INSIDE a tuple type, e.g. [first, ...rest].
// In .ts:  type WithId<T extends unknown[]> = [id: number, ...rest: T];
// At runtime, the array spread operator IS the value-level version. Watch it.

// Prepend an id to ANY array, keeping all original elements after it:
function withId(id, rest) {
  return [id, ...rest]; // [id, ...rest] — exactly the tuple shape above
}

console.log(withId(1, ["a", "b"])); // [ 1, 'a', 'b' ]
console.log(withId(7, [true]));     // [ 7, true ]
console.log(withId(9, []));         // [ 9 ]

// Append to the END instead — [...rest, last]:
function withFlag(rest) {
  return [...rest, "done"];
}
console.log(withFlag([1, 2, 3]));   // [ 1, 2, 3, 'done' ]

2. Typing concat & Labeled Elements

The headline use is a concat whose return type is [...T, ...U]: it joins two tuples while keeping every element's type and position, so result[0] stays a number. Labeled tuple elements like [x: number, y: number] add names for readability — type-only, never at runtime.

// concat keeps every element's type AND position via a variadic return tuple:
function concat<T extends unknown[], U extends unknown[]>(
  a: [...T],
  b: [...U],
): [...T, ...U] {
  return [...a, ...b];
}

const r = concat([1, 2], ["a", "b"]);
// r: [number, number, string, string]  — not (number | string)[]
const x: number = r[0]; // ✅ position 0 is known to be number

// LABELED tuple elements document each slot (names are type-only):
type Point = [x: number, y: number];
type Range = [start: number, end: number, step?: number];
// CONCAT: a variadic tuple type lets concat preserve the exact element types of
// BOTH inputs:  function concat<T extends [], U extends []>(a, b): [...T, ...U]
// At runtime that's just spreading both arrays into one.

function concat(a, b) {
  return [...a, ...b]; // [...a, ...b] mirrors the tuple type [...T, ...U]
}

console.log(concat([1, 2], ["a", "b"])); // [ 1, 2, 'a', 'b' ]
console.log(concat([true], [1, 2, 3]));  // [ true, 1, 2, 3 ]
console.log(concat([], ["only"]));        // [ 'only' ]

// Positions are preserved, so TS would know result[0] is number,
// result[2] is string — far more precise than a widened (number | string)[].
const r = concat([1, 2], ["a", "b"]);
console.log(r[0], r[2]);                  // 1 a

3. Rest Parameters as Tuples & Currying

TypeScript types a function's ...args as a tuple, so variadic tuples are what make bind, currying, and partial application type-safe. Splitting a parameter tuple into a head and tail — [...H, ...T] — lets you fix some arguments now and supply the rest later, each part fully typed.

// '...args' in a signature is a TUPLE type — variadic tuples make it precise:
type Args = Parameters<(a: string, b: number) => void>; // [string, number]

// A typed partial-application (like .bind) splits a tuple of args:
declare function partial<H extends unknown[], T extends unknown[], R>(
  fn: (...args: [...H, ...T]) => R,
  ...head: H
): (...tail: T) => R;

const greet = (greeting: string, name: string) => greeting + ", " + name;
const hi = partial(greet, "Hi");   // (name: string) => string
const result: string = hi("Ada");  // "Hi, Ada"  — tail args remain typed
// REST PARAMETERS are typed as a TUPLE, which is how TS strongly types things
// like bind() and curry(). Here is a tiny curry that splits a 2-arg function
// into two calls — the runtime behind that idea.

function curry(fn) {
  // fn expects (a, b). Return a function of 'a' that returns a function of 'b'.
  return (a) => (b) => fn(a, b);
}

const add = (a, b) => a + b;
const curriedAdd = curry(add);

console.log(curriedAdd(2)(3));   // 5
console.log(curriedAdd(10)(20)); // 30

// Partial application — fix the first argument, supply the rest later:
const add5 = curriedAdd(5);
console.log(add5(1));   // 6
console.log(add5(100)); // 105

🎯 Your Turn

Build the runtime behind prepend<H, T>(head, tail): [H, ...T] — put head first, then spread the rest. Fill in the two blanks marked ___, then run it.

// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
// In .ts:  function prepend<H, T extends unknown[]>(head: H, tail: T): [H, ...T]
// Build the runtime: put 'head' first, then spread the rest.

function prepend(head, tail) {
  return [head, ...___];           // 👉 spread the 'tail' array after head
}

console.log(prepend(0, [1, 2, 3])); // [ 0, 1, 2, 3 ]
console.log(prepend(___, []));       // 👉 pass head "x" with empty tail -> [ 'x' ]

// ✅ Expected output:
//    [ 0, 1, 2, 3 ]
//    [ 'x' ]

Common Errors (and the fix)

Pro Tips

📚 Keep Learning

Mini-Challenge: pair() and swap()

No blanks this time — just a brief and a starting outline. Build the tuple-preserving helpers yourself, run them, and check your output against the example in the comments.

// 🎯 MINI-CHALLENGE: tuple-preserving pair() and swap().
// In .ts:  function pair<A, B>(a: A, b: B): [A, B]
//          function swap<A, B>(t: [A, B]): [B, A]
// 1. pair(a, b) returns the two-element tuple [a, b].
// 2. swap(t) returns [t[1], t[0]] — positions flipped.
// 3. Log pair("name", 42), swap(pair("name", 42)), and swap([1, "two"]).
//
// ✅ Expected output:
//    [ 'name', 42 ]
//    [ 42, 'name' ]
//    [ 'two', 1 ]

// your code here

🎉 Lesson Complete

Practice quiz

A variadic tuple type is a tuple that includes:

  • At least five elements
  • Only optional elements
  • A spread element referencing a type parameter
  • No elements

Answer: A spread element referencing a type parameter. A spread inside a tuple type, like [first, ...rest], makes its length and contents vary with a type parameter.

The runtime equivalent of the type-level [...T, ...U] is:

  • The array spread operator
  • A for loop only
  • Object.assign
  • JSON.stringify

Answer: The array spread operator. Spreading both arrays into one ([...a, ...b]) mirrors the variadic tuple type exactly.

How many rest elements may a single tuple type contain?

  • Zero
  • Exactly two
  • Unlimited
  • At most one

Answer: At most one. A tuple type may have at most one rest element; [...string[], ...number[]] is an error.

Compared to (number | string)[], a variadic tuple [...T, ...U]:

  • Forgets positions
  • Keeps each element's type and position
  • Is always shorter
  • Cannot be returned

Answer: Keeps each element's type and position. A variadic tuple preserves exact types and positions, so result[0] can be known as number.

Labeled tuple elements like [x: number, y: number] are:

  • Purely for readability, type-only
  • Accessible at runtime as keys
  • Required for variadic tuples
  • A kind of union

Answer: Purely for readability, type-only. Labels document each slot for editors and readers but do not exist at runtime.

In TypeScript, a function's ...args rest parameter is typed as a:

  • Union
  • Record
  • Tuple
  • Promise

Answer: Tuple. Rest parameters are typed as a tuple, which is what makes typed bind, curry, and partial possible.

The spread element in a variadic tuple type may appear:

  • Only at the end
  • At the start, middle, or end
  • Only at the start
  • Never inside a tuple

Answer: At the start, middle, or end. Modern TypeScript allows the single rest element at the start, middle, or end.

[id: number, ...rest: T] where T is [string, boolean] describes:

  • [number]
  • [string, boolean]
  • (number | string | boolean)[]
  • [id: number, string, boolean]

Answer: [id: number, string, boolean]. The spread expands T's elements after the leading number, giving [id: number, string, boolean].

Parameters<(a: string, b: number) => void> is:

  • string | number
  • [string, number]
  • void
  • { a: string; b: number }

Answer: [string, number]. Parameters extracts the argument types as a tuple — here [string, number].

Variadic tuples are most useful whenever an array's:

  • Length is unknown
  • Elements are all identical
  • Positions matter
  • Values are strings

Answer: Positions matter. They avoid losing type precision through helpers when each position's type matters.

Continue this course

Frequently asked questions

What is a variadic tuple type?

It's a tuple type that includes a spread element referencing a type parameter, like [first: number, ...rest: T]. This lets the tuple's length and contents vary based on T, so you can model functions like concat that combine arrays while keeping each element's exact type and position. The runtime equivalent is the array spread operator (...).

How is a variadic tuple different from an array type?

An array type like (number | string)[] forgets how many elements there are and which position holds which type. A variadic tuple such as [...T, ...U] keeps each element's type and position, so result[0] can be known as number and result[2] as string. It's strictly more precise than a widened array.

What are labeled tuple elements?

Labeled tuples let you name each slot for documentation: [x: number, y: number] or [start: number, end: number, step?: number]. The labels are purely for readability and editor hints — they don't change the type and aren't accessible at runtime — but they make tuple-based APIs (and rest parameters) far easier to read.

Why are rest parameters relevant here?

TypeScript types a function's '...args' as a tuple, so variadic tuple types are what make things like Function.prototype.bind, currying, and partial application strongly typed. You can split a parameter tuple into a head and tail ([...H, ...T]) and type each piece independently, which is exactly how a typed partial() or curry() works.

Related lessons