TypeScript Tuples

A tuple in TypeScript is an array with a fixed length and a known type for each position. Where a regular array type like number[] means “a list of numbers, any length,” a tuple type like [string, number] means “exactly two elements: a string first, then a number.” Tuples let you model small, ordered, structured pieces of data — a coordinate pair, a key/value entry, an RGB color — with more precision than a plain array or a full interface would give you. This lesson covers tuple syntax, labeled and optional elements, rest elements, readonly tuples, how the compiler checks them, and the mistakes people commonly make.

Overview: How Tuples Work

Under the hood, TypeScript’s array type is really the generic Array<T>, where every element shares one type. A tuple is a special kind of array type where the compiler tracks a sequence of element types instead of one shared type — position 0 might be a string, position 1 a number, and so on. The type checker uses this positional information to verify three things: that you construct the tuple with the right number of elements in the right order, that reading a given index gives you the correct type, and that you don’t accidentally treat a tuple as if it were a same-typed array.

Crucially, none of this exists at runtime. JavaScript has no tuple type — a TypeScript tuple value is just a plain JavaScript array. This is type erasure: once the TypeScript compiler emits JavaScript, all tuple type annotations disappear, and what’s left is an ordinary array with whatever elements you put in it. The compiler’s job is entirely static — it front-loads checks so that by the time your code runs, you already know the shapes line up.

Tuple checking is positional, not just “does this look like an array of the right length.” Each element’s type must be assignable at its exact index — you can’t swap a [string, number] for a [number, string] even though both are two-element arrays. Since TypeScript 4.0, you can also give tuple elements labels (e.g. [x: number, y: number]) purely for readability and editor tooling — labels don’t change how the type behaves at all; they’re documentation baked into the type signature, similar to parameter names.

Syntax

The general forms of a tuple type declaration:

type Pair = [number, number];                 // fixed-length tuple
type Labeled = [x: number, y: number];         // labeled elements (for readability)
type WithOptional = [number, number?];         // optional trailing element
type WithRest = [string, ...number[]];         // rest element (variable-length tail)
type ReadonlyPair = readonly [number, number]; // immutable tuple
Form Meaning
[number, number] Exactly two elements, both numbers, in that order.
[x: number, y: number] Same as above, but each position has a descriptive label shown in tooltips and error messages.
[number, number?] Second element is optional; the tuple may have length 1 or 2.
[string, ...number[]] A required string first, followed by zero or more numbers (a rest element).
readonly [number, number] A tuple whose elements cannot be reassigned after creation, and which has no mutating array methods.

Examples

Example 1: A basic coordinate tuple.

let point: [number, number] = [10, 20];
console.log(point[0], point[1]);

point = [5, 15];
console.log(`x=${point[0]}, y=${point[1]}`);

Output:

10 20
x=5, y=15

The variable point is typed as exactly two numbers. Assigning a new array of the wrong length or wrong element types would be a compile error — only two-number arrays are valid replacements.

Example 2: Labeled tuple elements and destructuring.

type HttpResponse = [status: number, message: string, success: boolean];

function describe(response: HttpResponse): string {
  const [status, message, success] = response;
  return `${status}: ${message} (${success ? "ok" : "failed"})`;
}

const res: HttpResponse = [200, "OK", true];
console.log(describe(res));

const errorRes: HttpResponse = [404, "Not Found", false];
console.log(describe(errorRes));

Output:

200: OK (ok)
404: Not Found (failed)

The labels status, message, and success don’t force you to destructure with those exact names — they just make the type self-documenting in editor tooltips. Destructuring works the same as it would on any array.

Example 3: Optional elements.

type RGB = [red: number, green: number, blue: number];
type RGBA = [red: number, green: number, blue: number, alpha?: number];

function toCssColor([r, g, b, a]: RGBA): string {
  return a === undefined ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${a})`;
}

const solid: RGBA = [255, 0, 0];
const translucent: RGBA = [0, 128, 255, 0.5];

console.log(toCssColor(solid));
console.log(toCssColor(translucent));

Output:

rgb(255, 0, 0)
rgba(0, 128, 255, 0.5)

Because alpha is marked optional with ?, an RGBA tuple can have length 3 or 4. Inside the function, a has type number | undefined, so the code must handle the missing case explicitly.

Example 4: Rest elements for variable-length tails.

type Weekday = [name: string, ...scores: number[]];

const schedule: Weekday = ["Monday", 9, 10, 14];
console.log(schedule[0]);
console.log(schedule.slice(1));

Output:

Monday
[ 9, 10, 14 ]

A rest element (...scores: number[]) lets a tuple require a fixed prefix — here, exactly one string — while allowing any number of trailing elements of a given type. This is how variadic function parameter lists are typed internally.

Example 5: Readonly tuples.

function distance(from: readonly [number, number], to: readonly [number, number]): number {
  const dx = to[0] - from[0];
  const dy = to[1] - from[1];
  return Math.sqrt(dx * dx + dy * dy);
}

const origin = [0, 0] as const;
const target: [number, number] = [3, 4];

console.log(distance(origin, target));

Output:

5

as const turns the array literal into a readonly tuple of literal types, which is assignable wherever a readonly [number, number] is expected. Marking function parameters readonly signals — and enforces — that the function won’t mutate the tuple it’s given.

Under the Hood: How the Checker Handles Tuples

When you write [10, 20] without an annotation, TypeScript infers the widened type number[], not a tuple — inference only produces a tuple type when the context calls for one (an annotated variable, a typed function parameter, or as const). This is why annotations and as const matter so much with tuples: without them, the literal-position information is thrown away immediately.

There’s also a well-known gap in the type system: mutating methods like push, pop, and splice are still available on ordinary (non-readonly) tuples, and their signatures aren’t aware of the tuple’s fixed length. That means this compiles without any error:

const point: [number, number] = [10, 20];
point.push(99);
console.log(point.length);
console.log(point);

Output:

3
[ 10, 20, 99 ]

The tuple’s “fixed length” is a compile-time guarantee about how it’s typed and read, not a runtime lock on the underlying array. Marking a tuple readonly removes push/pop/splice entirely, which is the closest TypeScript gets to true immutability for tuples.

Common Mistakes

Mistake 1: expecting a returned array literal to be inferred as a tuple.

function makePair(a: number, b: number) {
  return [a, b];
}

const pair: [number, number] = makePair(1, 2);

This fails with an error similar to: Type 'number[]' is not assignable to type '[number, number]'. Because makePair has no explicit return type, TypeScript infers number[] from the array literal — widened, not tupled — and a plain array isn’t assignable to a fixed-length tuple. The fix is to declare the intended return type explicitly:

function makePair(a: number, b: number): [number, number] {
  return [a, b];
}

const pair: [number, number] = makePair(1, 2);
console.log(pair);

Output:

[ 1, 2 ]

With the return type annotated as a tuple, the checker validates the returned literal against that exact shape instead of widening it.

Mistake 2: indexing past the tuple’s declared length.

const point: [number, number] = [3, 4];
console.log(point[2]);

This fails with an error similar to: Tuple type '[number, number]' of length '2' has no element at index '2'. Unlike a plain number[], a tuple type only exposes the indices it declares, so reading past the end is caught statically rather than silently producing undefined at runtime. The fix is simply to stay within bounds — or to widen the type to an array if variable-length access is genuinely needed:

const point: [number, number] = [3, 4];
console.log(point[0], point[1]);

Output:

3 4

Best Practices

  • Always give a tuple-returning function an explicit return type annotation — without one, array literals widen to a same-typed array instead of a tuple.
  • Use labeled tuple elements ([x: number, y: number]) for anything read by another developer; the labels cost nothing at runtime but make signatures self-documenting.
  • Prefer readonly tuples for values that shouldn’t change shape — this also removes mutating methods like push/pop/splice, closing the gap where plain tuples silently allow extra elements.
  • Reach for as const when you want a literal array to be treated as a fixed, readonly tuple of literal types instead of a widened array.
  • Use tuples for small, fixed-shape, order-dependent data (coordinates, key/value pairs, function argument lists); switch to an interface or named object type once a shape has more than two or three fields or the meaning of a position isn’t obvious from context alone.
  • Use rest elements ([string, ...number[]]) instead of a plain array when a required prefix needs to be typed differently from the variable-length remainder.

Practice Exercises

  • Define a tuple type Coordinate3D representing an x, y, and z number, then write a function magnitude(point: Coordinate3D): number that returns the Euclidean distance from the origin. Test it with [3, 4, 0] and confirm it prints 5.
  • Define a labeled tuple type KeyValue for [key: string, value: number], then write a function that takes an array of KeyValue tuples and returns the sum of all the values.
  • Define a tuple type LogEntry as [level: "info" | "warn" | "error", message: string, ...tags: string[]]. Write a function that formats a LogEntry into a single string, e.g. "[warn] disk low (disk, ops)" from ["warn", "disk low", "disk", "ops"].

Summary

  • A tuple type is a fixed-length array where each position has its own, individually-tracked type.
  • Tuples are erased at runtime — the compiled JavaScript is a plain array with no trace of the tuple type.
  • Element labels ([x: number, y: number]) only improve readability and tooling; they don’t change type-checking behavior.
  • Optional elements (number?) and rest elements (...number[]) let a tuple have a variable length while still typing its fixed prefix precisely.
  • Array literals widen to plain arrays unless the context expects a tuple — annotate return types and variable types, or use as const, to keep tuple typing.
  • Readonly tuples (readonly [number, number]) prevent reassignment and remove mutating array methods, closing a known gap where plain tuples allow push/pop/splice without error.