Tuples
A tuple is a fixed-length array where the type of each element at each position is known. While a regular array like number[] only says "every element is a number," a tuple like [string, number, boolean] says "exactly three elements: first a string, then a number, then a boolean." Tuples bring positional type safety to JavaScript's otherwise untyped arrays.
Defining a Tuple
Declare a tuple by listing the types of each position inside square brackets:
// A tuple: exactly [string, number] let person: [string, number] = ['Alice', 30]; // Access elements — types are positional const name: string = person[0]; // string const age: number = person[1]; // number // Wrong order — caught at compile time let wrong: [string, number] = [30, 'Alice']; // Error: Type 'number' is not assignable to type 'string' // Type 'string' is not assignable to type 'number' // Wrong length — also caught let tooShort: [string, number] = ['Alice']; // Error: Type '[string]' is not assignable to type '[string, number]'
Tuples vs Arrays
Feature | Array (T[]) | Tuple ([T, U, V]) |
|---|---|---|
Length | Variable | Fixed |
Element types | All elements the same type | Each position has its own type |
Out-of-bounds access | Returns undefined at runtime | Compile error (with noUncheckedIndexedAccess) |
Use case | Lists of homogeneous items | Records with fixed structure |
Common example | number[] — array of scores | [string, number] — name + age pair |
Destructuring Tuples
Destructuring is the most natural way to work with tuples. TypeScript infers each variable's type from its position:
const color: [number, number, number] = [255, 128, 0];
const [red, green, blue] = color;
// red: number, green: number, blue: number
// Custom variable names make the meaning clear
const [r, g, b] = color;
console.log(`rgb(${r}, ${g}, ${b})`);
// Skip elements with _
const [, secondElement] = [10, 20, 30];
// secondElement: number — 20
// Rest in destructuring
const [head, ...tail]: [number, ...number[]] = [1, 2, 3, 4];
// head: number — 1
// tail: number[] — [2, 3, 4]Named Tuple Elements (TypeScript 4.0+)
Since TypeScript 4.0, you can label each position in a tuple. Labels improve readability and appear in IDE tooltips but do not change the type behaviour.
// Named tuple elements type Range = [start: number, end: number]; type RGB = [red: number, green: number, blue: number]; type HttpResult = [statusCode: number, body: string, headers: Record<string, string>]; const range: Range = [1, 100]; const pixel: RGB = [255, 0, 128]; // Hover in VS Code shows the label names in the tooltip // Makes code self-documenting without needing variable renaming tricks // Destructuring names are still arbitrary — labels are just hints const [from, to] = range; // from and to are valid names const [start, end] = range; // so are start and end
Optional Tuple Elements
Add a ? to mark the last one or more elements of a tuple as optional:
// Last element is optional type Point2D = [x: number, y: number]; type Point3D = [x: number, y: number, z?: number]; const p2: Point3D = [1, 2]; // OK — z is optional const p3: Point3D = [1, 2, 3]; // OK // TypeScript enforces: optional elements must come after required ones type Invalid = [x?: number, y: number]; // Error: A required element cannot follow an optional element
Rest Elements in Tuples
Tuples can have a rest element that captures a variable number of elements of a specific type:
// Leading required elements, rest at the end type StringWithNumbers = [string, ...number[]]; const s1: StringWithNumbers = ['hello']; // OK — no numbers required const s2: StringWithNumbers = ['hello', 1, 2, 3]; // OK const s3: StringWithNumbers = [1, 'hello']; // Error: first must be string // Rest at the start (TypeScript 4.2+) type NumbersWithString = [...number[], string]; const n1: NumbersWithString = ['end']; // OK const n2: NumbersWithString = [1, 2, 3, 'end']; // OK // Rest in the middle (TypeScript 4.2+) type Sandwich = [string, ...string[], string]; // First slice, any fillings, last slice
Readonly Tuples
Just like arrays, tuples can be made readonly to prevent mutation:
// Readonly tuple — prevents mutation
const point: readonly [number, number] = [10, 20];
point[0] = 99; // Error: Cannot assign to '0' because it is a read-only property
// as const creates a readonly tuple with literal types
const direction = ['north', 'east'] as const;
// type: readonly ['north', 'east']
direction[0] = 'south'; // Error
direction.push('west'); // Error: push does not exist on readonly tuple
// Useful for function return values you don't want callers to mutate
function getCoords(): readonly [number, number] {
return [48.8566, 2.3522]; // Paris
}Tuples as Function Return Values
One of the most practical uses of tuples is as return values from functions that need to return multiple values. The classic example is React's useState hook:
// Simulated useState — returns [value, setter] tuple
function useState<T>(initial: T): [T, (newValue: T) => void] {
let state = initial;
const setState = (newValue: T) => { state = newValue; };
return [state, setState];
}
const [count, setCount] = useState(0);
// count: number
// setCount: (newValue: number) => void
setCount(1); // OK
setCount('hello'); // Error: Argument of type 'string' is not assignable to type 'number'// Custom hook pattern — common in React
function useToggle(initialValue: boolean): [boolean, () => void] {
let value = initialValue;
const toggle = () => { value = !value; };
return [value, toggle];
}
const [isOpen, toggleOpen] = useToggle(false);
// isOpen: boolean
// toggleOpen: () => voidTuples as Function Parameters
Tuples can describe the shape of function argument lists, which is especially useful with spread syntax and rest parameters:
// Spread a tuple as function arguments
type Params = [string, number, boolean];
const args: Params = ['Alice', 30, true];
function createUser(name: string, age: number, admin: boolean) {
return { name, age, admin };
}
createUser(...args); // OK — TypeScript verifies the spread matches the signature
// Rest parameters that form a tuple
function log(level: 'info' | 'warn' | 'error', ...parts: [string, ...unknown[]]): void {
console.log(`[${level}]`, ...parts);
}Tuple Type Inference Gotcha
TypeScript does not infer tuple types by default — it infers regular array types. You need to force it with as const or an explicit annotation:
// Inferred as: (string | number)[] — NOT a tuple
const pair = ['Alice', 30];
// Fix 1: explicit annotation
const pair1: [string, number] = ['Alice', 30];
// Fix 2: as const — creates readonly tuple with literal types
const pair2 = ['Alice', 30] as const;
// type: readonly ['Alice', 30]
// Fix 3: helper function to infer tuple
function tuple<T extends unknown[]>(...args: T): T {
return args;
}
const pair3 = tuple('Alice', 30);
// type: [string, number]Practical Examples
Key-Value Pairs
type Entry<K, V> = [key: K, value: V];
type StringEntry = Entry<string, string>;
const entries: StringEntry[] = [
['name', 'Alice'],
['city', 'Paris'],
['lang', 'TypeScript'],
];
// Convert to an object
const obj = Object.fromEntries(entries);
// type: { [k: string]: string }Result Pattern (without exceptions)
// Inspired by Go-style error handling
type Result<T, E = Error> = [data: T, error: null] | [data: null, error: E];
async function trySomething(): Promise<Result<string>> {
try {
const data = await fetchData();
return [data, null];
} catch (e) {
return [null, e instanceof Error ? e : new Error(String(e))];
}
}
// Usage
const [data, error] = await trySomething();
if (error) {
console.error(error.message);
} else {
console.log(data.toUpperCase()); // TypeScript knows data is string here
}Coordinate Systems
type Coordinate2D = [x: number, y: number];
type Coordinate3D = [x: number, y: number, z: number];
type GeoCoord = [latitude: number, longitude: number];
function distance([x1, y1]: Coordinate2D, [x2, y2]: Coordinate2D): number {
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}
const origin: Coordinate2D = [0, 0];
const point: Coordinate2D = [3, 4];
console.log(distance(origin, point)); // 5Tuples vs Objects: When to Use Which
Scenario | Use Tuple | Use Object / Interface |
|---|---|---|
2-3 related values, position is obvious | Yes | No |
Many fields (4+) | No | Yes — names are essential |
Return value from a hook | Yes — [value, setter] | Sometimes — depends on complexity |
Public API function return | No — fragile | Yes — names prevent misuse |
CSV row / coordinate pair | Yes | Overkill for simple pairs |
Domain model with optional fields | No | Yes — optional properties are cleaner |
Summary
Tuples are fixed-length arrays where each position has its own type
Use named tuple elements (TypeScript 4.0+) to make positions self-documenting
Optional tuple elements (?) must come after all required elements
Rest elements allow variable-length tuples: [string, ...number[]]
Add readonly to prevent mutation: readonly [number, number]
TypeScript infers arrays, not tuples — use as const or explicit annotation to force it
Tuple return types are ideal for hook-style APIs: [value, setter]
For 4+ related values, prefer an object or interface — names matter at that point