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
- Spread a type parameter inside a tuple type
- Prepend and append with [first, ...rest] / [...rest, last]
- Type a concat that preserves both inputs' positions
- Read labeled tuple elements like [x: number, y: number]
- Understand rest parameters as tuple types
- Type a curry / partial-application helper
💡 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 a3. 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)
- ❌ "A rest element must be last in a tuple type" in old TS — you put a spread before fixed elements. ✅ Modern TS allows leading/middle spreads; ensure you're on TS 4.0+.
- ❌ "A tuple type cannot be indexed with..." — you indexed past a variadic part's known positions. ✅ Only fixed positions have a precise type; the spread region is element-typed.
- ❌ Two rest elements — [...string[], ...number[]] is invalid. ✅ A tuple type may contain at most one rest element.
- ❌ Result widens to an array — you annotated the return as T[]. ✅ Return the variadic tuple [...T, ...U] to keep positions.
Pro Tips
- 💡 Label your tuple slots — [x: number, y: number] reads far better than two anonymous numbers.
- 💡 Combine with Parameters<F> to build typed pipe, compose, and partial utilities.
- 💡 Constrain spreads with T extends unknown[] so callers must pass an array-like tuple.
- 💡 Reach for variadic tuples whenever an array's positions matter — they're how you avoid losing type precision through a helper.
📚 Keep Learning
- 📖 TS 4.0 Release Notes — Variadic Tuple Types
- 📖 TS 4.0 — Labeled Tuple Elements
- 📖 Handbook — Tuple Types
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
- ✅ A variadic tuple type spreads a type parameter inside a tuple
- ✅ [first, ...rest] and [...rest, last] prepend and append precisely
- ✅ A concat returning [...T, ...U] keeps every element's type and position
- ✅ Labeled tuple elements ([x: number, y: number]) document slots — type-only
- ✅ Rest parameters are tuples, which is how curry / partial get typed
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
- Previous: Function Types, this, Parameters & ReturnType
- Next: Advanced Utility Types — Awaited, Parameters, ReturnType, NonNullable — and build your own with infer
- Quick reference: TypeScript cheat sheet
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.