Suggest an editImprove this articleRefine the answer for “Tuple types in TypeScript”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Tuple types** in TypeScript are fixed-length arrays where each position has a specific type. Unlike regular arrays, the compiler tracks the type at every index separately and enforces the length at compile time. ```typescript const user: [string, number] = ["Alice", 25]; user[0]; // string user[1]; // number user[2]; // Error: no element at index '2' ``` **Key:** at runtime, tuples are plain JavaScript arrays. All type enforcement happens only in the compiler.Shown above the full answer for quick recall.Answer (EN)Image**Tuple types** in TypeScript are fixed-length arrays where each position has its own specific type. ## Theory ### TL;DR - A tuple is a typed array where position matters: `[string, number]` means index 0 is always `string`, index 1 is always `number` - Unlike a regular array, the compiler enforces both the count and the type at each slot - `useState` in React returns a tuple: `[value, setter]` - Named elements (`[x: number, y: number]`) add readability without changing runtime behavior - Use tuples for function return values and structured pairs, not for lists of unknown length ### Quick example ```typescript // Array: any number of strings, all the same type const tags: string[] = ["ts", "react", "node"]; // Tuple: exactly 2 elements, a specific type per position const user: [string, number] = ["Alice", 25]; user[0]; // string user[1]; // number user[2]; // Error: Tuple type '[string, number]' has no element at index '2' // TypeScript catches this at compile time, not runtime const wrong: [string, number] = [25, "Alice"]; // Error ``` The compiler knows the exact type at each index. `user[0]` is `string`, not `string | number`. ### Tuple vs Array A regular array (`string[]`) says: every element is a string, any count. A tuple (`[string, number]`) says: exactly two elements, first is string, second is number. That single difference changes how the compiler reasons about each slot. With a plain array, `arr[0]` returns `string`. With a tuple, `tup[0]` also returns `string`, but `tup[1]` returns `number`. The compiler tracks each position independently. ### Named elements ```typescript type Point = [x: number, y: number]; type Range = [start: number, end: number]; type HttpResponse = [status: number, body: string]; const origin: Point = [0, 0]; ``` Names exist for humans, not the compiler. `Point` and `[number, number]` are structurally identical. But destructuring `const [x, y] = origin` will show `x` and `y` in IDE tooltips. Worth adding on any tuple with 3 or more elements. ### Optional and rest elements ```typescript // Optional element (must be last) type Response = [number, string, string?]; const minimal: Response = [200, "OK"]; const full: Response = [200, "OK", "application/json"]; // Rest elements: first is string, then any number of numbers type StringThenNumbers = [string, ...number[]]; const scores: StringThenNumbers = ["player1", 90, 85, 92]; // Fixed ends, flexible middle type Padded = [string, ...number[], boolean]; const row: Padded = ["label", 1, 2, 3, true]; ``` Optional elements must come last. Only one rest element per tuple, but it can sit in the middle. ### Readonly tuples ```typescript type ReadonlyPoint = readonly [number, number]; const p: ReadonlyPoint = [10, 20]; p[0] = 30; // Error: Cannot assign to '0' because it is a read-only property // as const creates a readonly tuple with literal types const coords = [10, 20] as const; // readonly [10, 20] ``` `as const` goes further than `readonly`: it narrows the types to literal values (`10` and `20`, not `number`). That matters when a function signature expects `readonly [10, 20]` specifically vs just `readonly [number, number]`. ### How the compiler handles tuples TypeScript compiles tuples to plain JavaScript arrays. There is no tuple object at runtime. Length checks, per-index types, readonly constraints: all of that lives only in the type checker. Because of this, calling `.push()` on a non-readonly tuple compiles without error in some cases. The compiler checks index access but array mutation methods can pass through. Use `readonly` to block them. ### Common mistakes **Forgetting to annotate, getting an inferred union array:** ```typescript const pair = ["Alice", 25]; // TypeScript infers: (string | number)[] — not a tuple! function greet(person: [string, number]) {} greet(pair); // Error: Argument of type '(string | number)[]' is not // assignable to parameter of type '[string, number]' // Fix: annotate explicitly const pair: [string, number] = ["Alice", 25]; ``` **Mutating a non-readonly tuple:** ```typescript const coords: [number, number] = [10, 20]; coords.push(30); // Compiles fine, breaks the length contract at runtime const safe: readonly [number, number] = [10, 20]; safe.push(30); // Error: Property 'push' does not exist on type 'readonly [number, number]' ``` **Mixing up `as const` and explicit annotation:** ```typescript const a = [1, "hello"] as const; // readonly [1, "hello"] — literal types const b: [number, string] = [1, "hello"]; // [number, string] — wider types // a[0] is type '1', b[0] is type 'number' ``` Use `as const` when you need literal types. Use explicit annotation when you need the wider type. **Optional element in the wrong position:** ```typescript type Wrong = [string, number?, boolean]; // Error: A required element cannot follow an optional element type Right = [string, boolean, number?]; // OK ``` ### Real-world usage - React `useState` returns `[S, Dispatch<SetStateAction<S>>]`, a tuple - `useReducer` returns `[state, dispatch]` - Go-style error returns: `function fetchUser(): Promise<[User | null, Error | null]>` - D3 and charting libraries use `[number, number]` for coordinate pairs - Variadic tuple types power typed `compose` and `pipe` in functional libraries ### Follow-up questions **Q:** Why does TypeScript infer `(string | number)[]` instead of `[string, number]` when I write `const x = ["Alice", 25]`? **A:** TypeScript widens array literals to the most general type because arrays are mutable and can change length. To get a tuple, annotate explicitly or use `as const`. **Q:** What is a variadic tuple type? **A:** Variadic tuples use spread in type position: `type Concat<A extends unknown[], B extends unknown[]> = [...A, ...B]`. This lets you build new tuple types by combining others. Libraries use it for typed `compose`, `pipe`, and middleware chains where the full argument list needs to be tracked across function boundaries. **Q:** Can tuples extend other tuples? **A:** There is no `extends` for tuples the way classes have it. But you can spread one into another: `type Extended = [...Base, string]`. That is how variadic tuples work. **Q:** What happens when you call `.push()` on a readonly tuple? **A:** TypeScript errors: `Property 'push' does not exist on type 'readonly [number, number]'`. On a non-readonly tuple, `.push()` compiles but breaks the length contract at runtime. **Q:** How does destructuring a tuple differ from destructuring an object? **A:** With tuples, names come from the destructuring pattern, not the type. `const [a, b] = point` works regardless of named elements. With objects, property names are part of the type contract itself. ## Examples ### Parsing and returning coordinate pairs ```typescript function parseLatLng(input: string): [number, number] | null { const parts = input.split(",").map(Number); if (parts.length !== 2 || parts.some(isNaN)) return null; return [parts[0], parts[1]]; } const result = parseLatLng("48.8566,2.3522"); if (result) { const [lat, lng] = result; console.log(`Paris: ${lat}, ${lng}`); } ``` A tuple works better than an object here because the relationship is purely positional and the pair will never grow additional fields. The call site destructures naturally. ### Go-style error handling ```typescript async function fetchUser(id: string): Promise<[User | null, Error | null]> { try { const user = await db.users.findById(id); return [user, null]; } catch (err) { return [null, err instanceof Error ? err : new Error(String(err))]; } } const [user, error] = await fetchUser("123"); if (error) { console.error(error.message); return; } // TypeScript knows user is not null at this point console.log(user.name); ``` This pattern makes error handling visible at the call site without wrapping every call in try-catch. The convention teams agree on: either the first or the second element is null, never both. TypeScript does not enforce that rule automatically. ### Typed event system with variadic tuples ```typescript type EventMap = { resize: [width: number, height: number]; keydown: [key: string, modifiers: string[]]; submit: [data: FormData]; }; function emit<K extends keyof EventMap>(event: K, ...args: EventMap[K]): void { // dispatch args to registered listeners } emit("resize", 1920, 1080); // OK emit("keydown", "Enter", ["ctrl"]); // OK emit("resize", "wide", 1080); // Error: 'string' not assignable to 'number' ``` The event name and its argument types are linked in one place. Add a new event to the map and the compiler checks every `emit` call across the codebase automatically. This is where tuple types earn their place in a large project.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.