In TypeScript, what does the annotation `[string, number]` guarantee about a value that the annotation `(string | number)[]` does not?
answer
- shape, not just element type
- how many, and in what order
- length is part of the type
- destructuring keeps per-slot types
- the useState pair
basics
~20 sA tuple type fixes both the length and the type of each position: [string, number] accepts exactly two elements, a string then a number. (string | number)[] accepts any number of elements, in any order.
solid answer
~50 s`[string, number]` is a **tuple type**. It says the value is an array of exactly two elements where element 0 is a `string` and element 1 is a `number`, so `['a', 1]` is assignable but `[1, 'a']`, `['a']` and `['a', 1, 2]` are all errors. `(string | number)[]` only constrains the *element* type — every entry must be a string or a number, but the array may be empty, may hold twenty items, and may mix the two in any order. The practical consequences are indexing and destructuring: on the tuple, `t[0]` is `string` and `t[1]` is `number`, and `const [name, age] = t` types each binding precisely, whereas on the union array every read is `string | number` and you have to narrow it. Both are erased at compile time; at runtime each is just an array.
code
typescript · 9 linestype Point = [number, number];
const p: Point = [10, 20];
const [x, y] = p; // x: number, y: number
// p = [10, 20, 30]; // Error: 3 elements is not assignable to a 2-tuple
const loose: (number | string)[] = [10, '20', 30]; // any length, any order
const first = loose[0]; // number | string — needs narrowing before usego deeper
Be ready to say that a tuple fixes both how many elements there are and what type sits at each position, and to write one: [string, number] versus (string | number)[].
Explain the mechanics: length is a literal type on a tuple, indexing out of range is an error, assignability runs only from tuple to array-of-union, and destructuring keeps per-position types.
Show judgment about where a tuple is the right model — positional returns the caller renames, entry pairs — and point out that nothing validates the shape at runtime, so external data still needs a real parse step.
Own the API-design tradeoff: positional tuples are cheap and destructure well but encode meaning in order, which makes them brittle to extend. Set a team rule for when a named object type is required instead.
## What a tuple type is A **tuple type** is an array type in which the number of elements is fixed and each position carries its own type. You write it as a bracketed, comma-separated list of types: ```typescript type Point = [number, number]; type Entry = [string, number]; ``` There is no `Tuple` class and no new runtime value here. TypeScript's types are erased during compilation, so `const p: Point = [1, 2]` emits exactly `const p = [1, 2]` — an ordinary JavaScript array. The tuple type is a *promise the checker enforces at compile time*, nothing more. ## The contrast with an array of a union `(string | number)[]` (equivalently `Array<string | number>`) makes one statement: every element of this array is a string or a number. It says nothing about how many elements there are or what order they come in. All of these satisfy it: `[]`, `['a']`, `[1, 2, 3]`, `['a', 1, 'b', 2]`. `[string, number]` makes three statements at once: there are exactly two elements, position 0 is a string, position 1 is a number. Only `['a', 1]` — and values structurally identical to it — satisfy that. ```typescript const loose: (string | number)[] = ['a', 1, 'b']; // fine const tuple: [string, number] = ['a', 1]; // fine // const bad1: [string, number] = [1, 'a']; // wrong order // const bad2: [string, number] = ['a']; // too few elements // const bad3: [string, number] = ['a', 1, 2]; // too many elements ``` Assignability runs one way and only under conditions: a `[string, number]` is assignable to `(string | number)[]`, because a two-element array of a string and a number is indeed an array whose elements are strings or numbers. The reverse is rejected — the checker cannot know a `(string | number)[]` has exactly two elements in the right order. ## Length is part of the type Because the length is fixed, the compiler models it as a literal type. `Point['length']` is the type `2`, not `number`. That also means an out-of-range literal index is a compile error rather than a silently-typed read: ```typescript const p: [number, number] = [1, 2]; const a = p[1]; // number // const b = p[2]; // Error: tuple of length 2 has no element at index 2 ``` On `(string | number)[]`, `arr[99]` type-checks and is typed `string | number` — the checker has no length information to object with. (The `noUncheckedIndexedAccess` compiler flag changes what an index read is typed as on arrays, but it does not give a plain array a length guarantee.) ## Why tuples exist: positional returns and destructuring The reason tuples earn their place is the pattern where a function returns several related values and the *caller* names them. React's `useState` is the widely-known example: the hook returns a two-element value, and every call site writes `const [value, setValue] = useState(0)` with names of its own choosing. That only type-checks precisely because the return type is a tuple — each destructured binding picks up the type of its position: ```typescript function parseRange(input: string): [number, number] { const [lo, hi] = input.split('-'); return [Number(lo), Number(hi)]; } const [start, end] = parseRange('3-9'); // both number ``` With `(string | number)[]` as the return type, `start` and `end` would each be `string | number` and every use would need narrowing first. The same reasoning explains why the standard `Object.entries` result is modelled as an array of `[string, T]` pairs: each pair has a fixed shape even though the outer array does not. ## What the annotation does not do A tuple type is checked where the compiler can see the value being constructed or assigned. It performs **no runtime validation**. Data parsed from JSON, read from a CSV row, or pushed through an `as [string, number]` assertion is simply believed; if the real array has one element, nothing throws at the boundary and the failure appears later, where the code reads a position that does not exist. If the value crosses a trust boundary, validate it at runtime yourself and only then describe it with the tuple type. ## When to prefer an object Tuples read well when the positions are self-evident (a coordinate pair, a key/value entry) or when the caller renames them at destructuring time. Once you have three or four heterogeneous slots whose meaning depends on remembering the order, an object with named properties is the clearer model — the compiler protects you either way, but only one of the two documents itself.
- For `const t: [string, number]`, what is the type of `t[1]`, and what happens if you write `t[2]`?`t[1]` is `number` — each position is typed individually. `t[2]` is a compile error: the compiler reports that a tuple type of length 2 has no element at index 2. That out-of-range check is possible only because the length is part of the type; on a plain `number[]`, `arr[2]` type-checks silently.
- Does a tuple type cost anything at runtime?Nothing. TypeScript erases types during compilation, so a tuple-annotated value emits as an ordinary array literal. There is no wrapper object, no length check and no reflection. That also means a value arriving from JSON or an `as` assertion is never verified against the tuple shape — only code the compiler can see is checked.
- When would you model a pair as an object instead of a tuple?When the positions are not self-explanatory or there are more than two or three of them. A tuple's meaning lives in the order, which the reader has to remember; `{ min, max }` carries its meaning in the property names. Tuples stay worthwhile where the caller renames at destructuring time, like a state/setter pair.
An array-of-union is a bag with a rule about what may go in it; a tuple is an egg carton — a fixed number of slots, and each slot only takes the one thing it is shaped for.
saying these in an interview costs you the question
- Says a tuple is just an array with nicer syntax
- Thinks (string | number)[] enforces a two-element shape
- Expects the tuple type to check length at runtime
- Believes any (string | number)[] is assignable to [string, number]
- Claims t[2] on a two-element tuple type-checks fine