A regular array in TypeScript holds any number of items, and every item shares one type — number[] is a list of numbers of unknown length. But sometimes you need the opposite: a fixed number of items where each position means something specific. A pair of [latitude, longitude]. A [statusCode, message] result. That is exactly what typescript tuples give you — a typed array of fixed length where each slot can have its own type. This guide explains what tuples are, how they differ from ordinary arrays, and how to use the more advanced forms — labeled, optional, rest, and readonly tuples — without tripping over the rough edges.
What Is a Tuple in TypeScript?
A tuple is an array type with a fixed length and a known type at each position. You declare one by listing the element types inside square brackets:
let point: [number, number] = [40.7128, -74.0060];
Here point must have exactly two elements, both numbers. The first is the latitude, the second the longitude. TypeScript now enforces three things at compile time: the length is two, position 0 is a number, and position 1 is a number. Try to assign [40.7128] and you get an error — the tuple is missing an element. Try [40.7128, "west"] and the string at position one fails too.
The positions can hold different types, which is where tuples become genuinely useful:
let user: [number, string, boolean] = [1, "ada", true];
const id = user[0]; // number
const name = user[1]; // string
const active = user[2]; // boolean
TypeScript tracks the type of each index individually. user[0] is a number, user[1] is a string — the compiler knows which type lives at which position. That is the core difference from an array, where every element collapses to a single shared type.
Tuples vs Arrays: The Key Difference
It is easy to confuse the two because the runtime representation is identical — a tuple is a JavaScript array once compiled. The difference lives entirely in the type system. Compare:
let scores: number[] = [90, 85, 100]; // array: any length, all numbers
let pair: [string, number] = ["age", 30]; // tuple: exactly 2, typed per slot
With the array, scores.push(72) is fine and scores[10] is typed number even though it does not exist. With the tuple, length and per-position types are locked. The table below sums up how they behave:
| Behavior | Array (T[]) | Tuple ([A, B]) |
|---|---|---|
| Length | Variable | Fixed (with exceptions for rest/optional) |
| Type per index | One shared type | Distinct type per position |
[0], [1] access | Same type everywhere | Specific type at each index |
| Best for | Homogeneous collections | Structured, positional data |
Reach for a tuple when position carries meaning — a coordinate, a key/value pair, a function returning two related values. Reach for an array when you have a list of like items whose count you do not know up front. If you work with regular arrays often, the JavaScript array methods guide covers the operations that apply to both.
Destructuring Tuples
Because positions are meaningful, you almost always want to pull tuple elements into named variables. Destructuring does this cleanly and preserves the per-position types:
function getCoordinates(): [number, number] {
return [40.7128, -74.0060];
}
const [lat, lng] = getCoordinates();
// lat: number, lng: number
This is the pattern behind React’s useState, which returns a tuple of [value, setterFunction]:
const [count, setCount] = useState(0);
// count: number, setCount: (n: number) => void
The two elements have completely different types — a value and a function — yet destructuring assigns each correctly because TypeScript knows the tuple’s shape. The same destructuring syntax from JavaScript applies; tuples just make the resulting types precise.
Labeled Tuple Elements
A bare [number, number] does not tell a reader what each number means. Since TypeScript 4.0 you can attach labels to tuple positions. The labels are purely for documentation and editor tooltips — they do not change the type — but they make intent obvious:
type Coordinate = [latitude: number, longitude: number];
function distance(from: Coordinate, to: Coordinate): number {
// editor now shows `latitude` and `longitude` in hints
return Math.hypot(to[0] - from[0], to[1] - from[1]);
}
Labels are especially valuable on function rest parameters, where they show up in signature help as you type the call. If a position is optional, the label goes before the ?: [first: string, second?: string].
Optional and Rest Elements
Tuples are not always rigidly fixed. Two features let them flex while staying typed.
An optional element, marked with ?, may be present or absent. It must come after all required elements:
type HttpResult = [status: number, body: string, headers?: Record<string, string>];
const ok: HttpResult = [200, "OK"]; // headers omitted
const full: HttpResult = [200, "OK", { "x-trace": "abc" }]; // headers present
A rest element uses the spread syntax to allow a variable number of trailing items of one type — a fixed prefix followed by an open-ended tail:
type CommandArgs = [command: string, ...flags: string[]];
const a: CommandArgs = ["build"];
const b: CommandArgs = ["build", "--watch", "--minify"];
Here the first element is always a required string, and everything after is zero or more string flags. This is exactly how you’d type a function that takes a leading argument plus an arbitrary number of follow-ups. Rest elements can also sit in the middle — [string, ...number[], boolean] — and TypeScript still resolves the leading and trailing fixed positions correctly.
Readonly Tuples
By default a tuple’s elements can be reassigned, and mutating methods like push are technically allowed (a long-standing rough edge covered below). A readonly tuple freezes it at the type level:
const origin: readonly [number, number] = [0, 0];
origin[0] = 5; // Error: cannot assign to a readonly element
origin.push(1); // Error: push does not exist on a readonly tuple
Use readonly whenever a tuple represents a constant or a value you pass around but never intend to change — coordinates, RGB colors, configuration pairs. It documents intent and lets the compiler catch accidental mutation. A related shortcut is the as const assertion, which infers the narrowest possible readonly tuple from a literal:
const rgb = [255, 128, 0] as const;
// type: readonly [255, 128, 0]
Without as const, [255, 128, 0] is inferred as number[] — a plain array, not a tuple. The assertion is the most common way to get a tuple type out of an array literal without writing the annotation by hand.
A Practical Example: Typed Return Values
The clearest real-world use for tuples is returning more than one value from a function when wrapping them in an object would be overkill. A common pattern is the “result tuple” — return either an error or a value:
async function fetchUser(id: string): Promise<[Error, null] | [null, User]> {
try {
const res = await fetch(`/api/users/${id}`);
const user: User = await res.json();
return [null, user];
} catch (err) {
return [err as Error, null];
}
}
const [error, user] = await fetchUser("42");
if (error) {
console.error(error.message);
} else {
console.log(user.name); // user is non-null here
}
The union of two tuples — [Error, null] or [null, User] — lets TypeScript narrow correctly: once you check if (error), the compiler knows user is a real User in the else branch. This errors-as-values style pairs naturally with the typed async/await patterns you’d use for the fetch itself. If you prefer naming the fields, an object or interface is the alternative — tuples win when the values are few and their meaning is obvious from position.
Common Pitfalls With TypeScript Tuples
Mutating methods bypass the length guarantee. This is the biggest surprise. A non-readonly tuple still permits push, pop, and splice, because tuples inherit Array methods. TypeScript does not flag this:
const pair: [number, number] = [1, 2];
pair.push(3); // allowed — no compile error
// pair is now [1, 2, 3] at runtime, breaking the "length 2" contract
The fix is readonly, which removes the mutating methods from the type. If a tuple is meant to stay fixed, mark it readonly.
Forgetting as const gives you an array. As shown above, a plain array literal infers T[], not a tuple. If you expect tuple behavior and get loose array typing, an as const or an explicit annotation is almost always the missing piece.
Index access beyond the known length widens the type. Accessing an out-of-range index on a tuple with a rest element returns the rest element’s type rather than an error, so do not rely on indexing to catch length mistakes — destructure or check length instead.
Order is load-bearing. Because positions carry meaning, swapping two same-typed elements — [longitude, latitude] instead of [latitude, longitude] — produces no type error but a real bug. Labeled elements and named destructuring are your defense here.
Frequently Asked Questions
What is the difference between a tuple and an array in TypeScript?
An array (number[]) has a variable length and one shared element type. A tuple ([number, string]) has a fixed length with a specific type at each position. At runtime both are JavaScript arrays — the distinction exists only in TypeScript’s type system, which enforces the tuple’s length and per-index types at compile time.
Can a TypeScript tuple have optional elements?
Yes. Mark a position with ? to make it optional, as in [number, string?]. Optional elements must come after all required ones. The tuple is then valid both with and without that trailing element, and TypeScript tracks which positions might be undefined.
How do I make a tuple immutable?
Add the readonly modifier: readonly [number, number]. This removes mutating methods like push and splice from the type and forbids reassigning elements. The as const assertion on an array literal — [1, 2] as const — produces a readonly tuple automatically with the narrowest literal types.
Why can I still call push() on a tuple?
Tuples inherit Array’s prototype methods, so a non-readonly tuple permits push, pop, and splice even though they break the fixed-length contract. TypeScript does not flag these calls. Use a readonly tuple to remove the mutating methods and keep the length guarantee enforced.
When should I use a tuple instead of an object?
Use a tuple when you have a small, fixed set of values whose meaning is clear from their position — coordinates, a key/value pair, or a function returning two related values like React’s useState. Use an object or interface when there are several fields, when names add clarity, or when the order should not matter.
Where to Go From Here
Tuples are one of the smaller features in TypeScript’s type system, but they pull their weight wherever position carries meaning — coordinate pairs, paired return values, and the [value, setter] shape that powers React hooks. The patterns worth practicing next: using as const to derive precise tuple types from literals, applying readonly to lock down values you pass around, and pairing result tuples with narrowing to model success-or-error flows.
From here, deepen the surrounding type knowledge: see how interface and type differ for modeling object-shaped data, and review arrow functions in TypeScript since tuple destructuring shows up constantly in their parameter lists and return values. The official TypeScript handbook on tuple types and the MDN reference on array destructuring round out the runtime side of how this all behaves once compiled to JavaScript.