Typing Arrays and Tuples
Learn every way TypeScript can type arrays and tuples — from simple number[] arrays to readonly, variadic, and named tuples — plus how to type Map, Set, and Array.reduce correctly.
Arrays are everywhere in JavaScript, and TypeScript gives you a surprisingly rich set of tools for describing them precisely — including the tuple type, which lets you type a fixed-length array where each position has its own specific type, like coordinates or key-value pairs.
Typing Arrays
The most common way to type an array is with a trailing [] after the element type: number[] is an array of numbers, string[] an array of strings, and { id: number; name: string }[] an array of objects matching that shape. TypeScript also supports an equivalent generic syntax, Array<number>, which means exactly the same thing as number[] — the two are fully interchangeable, though the [] form is more common in everyday code and the Array<T> form is sometimes preferred for more complex element types to keep things readable.
Arrays can be nested to any depth: number[][] is an array of arrays of numbers (like a grid or matrix), and string[][][] would be a three-dimensional nested array — TypeScript checks the shape all the way down. By default, arrays are mutable (you can push, pop, or reassign elements), but you can opt into an immutable array type with readonly number[] (or the equivalent ReadonlyArray<number>), which disallows any mutating method or index assignment at compile time — a great fit for data that should never change after creation, like configuration lists or constants.
• let scores: number[] = [10, 20, 30]; // shorthand syntax • let names: Array<string> = ["Ada", "Grace"]; // equivalent generic syntax • let grid: number[][] = [[1, 2], [3, 4]]; // nested array • const ids: readonly number[] = [1, 2, 3]; // ids.push(4) is a compile error
Tuples: Fixed-Length, Position-Typed Arrays
A tuple is a special array type with a fixed number of elements where each position has its own specific type — unlike a regular array, where every element must share one common type. [number, number] describes an array with exactly two numbers, like a 2D coordinate; [string, number, boolean] describes an array with exactly three elements in that exact order and those exact types. This is far more precise than (string | number | boolean)[], which would allow any mix, any order, and any length.
Tuples support several extra features regular arrays don't. Optional elements use ?, like [string, number?] for a tuple whose second element may be omitted. Rest elements use ..., like [string, ...number[]] for a tuple that must start with a string but can be followed by any number of additional numbers — this is called a 'variadic tuple type' when used generically to build flexible function signatures. Named tuple elements, like [x: number, y: number], don't change the runtime behavior at all but add readable labels that show up in editor tooltips and autocomplete, which is especially useful for tuples with several same-typed positions where position alone ([number, number, number]) would be confusing. Finally, readonly [number, number] locks a tuple against any mutation, just like readonly does for arrays.
• const point: [number, number] = [10, 20]; // exactly two numbers, in order • const entry: [string, number?] = ["score"]; // optional second element • const row: [string, ...number[]] = ["total", 1, 2, 3]; // rest tuple elements • const coord: [x: number, y: number] = [10, 20]; // named tuple elements (labels only, for readability) • const frozen: readonly [number, number] = [0, 0]; // cannot be mutated
Map, Set, and Array.reduce Typing
Map and Set are built-in JavaScript collection types, and TypeScript types them generically. Map<K, V> describes a map whose keys are type K and values are type V — for example, Map<string, number> for a map from names to scores. Set<T> describes a set holding only values of type T, like Set<string> for a collection of unique names. Both are typically created with new Map<string, number>() or new Map() and let inference figure out the types from how you use them afterward.
Array.prototype.reduce is one of the trickier built-in methods to type correctly because its accumulator's type isn't always obvious from the array's element type alone. When the accumulator's final type matches the array's element type, TypeScript infers it automatically; when it doesn't (for example, reducing an array of numbers down to a single object), you typically need to supply the accumulator type explicitly as reduce's generic type parameter, often alongside an explicit initial value, so TypeScript knows what shape to expect on every step.
• const scoresByName = new Map<string, number>(); scoresByName.set("Ada", 95); • const uniqueTags = new Set<string>(["a", "b", "a"]); // Set<string> with 2 members • const total = [1, 2, 3].reduce((sum, n) => sum + n, 0); // inferred: number • const counts = [1, 2, 2, 3].reduce<Record<number, number>>((acc, n) => { acc[n] = (acc[n] ?? 0) + 1; return acc; }, {});
Arrays vs tuples, side by side
point.push(30) is allowed by the TypeScript array/tuple type system at compile time for a mutable tuple (tuples don't block push the way length-checks might suggest) — use readonly tuples when you need to forbid mutation entirely. Named tuple elements (x:, y:) are purely a readability aid for tooling — they don't change what values are valid.
// arrays: any length, every element shares one type
const scores: number[] = [10, 20, 30, 40];
const matrix: number[][] = [[1, 2], [3, 4]];
const frozenIds: readonly number[] = [1, 2, 3];
// tuples: fixed length, each position has its own type
const point: [number, number] = [10, 20];
const labeled: [x: number, y: number] = [10, 20]; // same values, readable labels
const optionalEntry: [string, number?] = ["score"];
const variadicRow: [string, ...number[]] = ["totals", 1, 2, 3];
const frozenPoint: readonly [number, number] = [0, 0];Code Examples
point is typed as a tuple of exactly two numbers, which is far more precise than number[] — a plain number[] would happily allow one element, three elements, or any other length. The function destructures the tuple directly in its parameter list, which TypeScript checks positionally against [number, number]. Expected result: 22.360679774997898 const point: [number, number] = [10, 20]; — fixes the array at exactly two number-typed positions.
Map<string, number> and Set<string> pin down exactly what keys/values or members are allowed. The first reduce call has its accumulator type inferred automatically since it matches the array's element type (number). The second reduce produces a completely different shape (a Record<string, number> count map) from a string[] input, so it needs the explicit <Record<string, number>> generic to tell TypeScript what the accumulator should look like at every step. Expected result: 19 { a: 3, b: 2, c: 1 }
const point: [number, number] = [10, 20];
function distanceFromOrigin([x, y]: [number, number]): number {
return Math.sqrt(x * x + y * y);
}
console.log(distanceFromOrigin(point));
// point = [10, 20, 30]; // Error: Source has 3 elements but target allows only 2const inventory = new Map<string, number>();
inventory.set("apples", 12);
inventory.set("oranges", 7);
const fruitNames = new Set<string>(inventory.keys());
const totalItems = [...inventory.values()].reduce(
(sum: number, count: number) => sum + count,
0
);
const grouped = ["a", "b", "a", "c", "b", "a"].reduce<Record<string, number>>(
(acc, letter) => {
acc[letter] = (acc[letter] ?? 0) + 1;
return acc;
},
{}
);
console.log(totalItems, grouped);Array vs tuple typing
An array type constrains every element to one shared type with no length limit; a tuple type constrains each position individually and fixes the overall length.
number[] [number, number]
| any length | exactly 2 elements
| every element: number | position 0: number
v | position 1: number
[1, 2, 3, 4, 5, ...] <- OK v
[10, 20] <- OK
[10, 20, 30] <- Error: too many elementsImportant Rules
Use T[] (or the equivalent Array<T>) for lists of unknown length where every element shares one type.
Use a tuple [A, B, C] when you need a fixed number of elements where each position has its own specific type and meaning.
Add readonly to arrays or tuples that should never be mutated after creation.
Give reduce an explicit generic type parameter whenever the accumulator's shape differs from the array's element type.
Where this shows up
React's useState hook returns a tuple — const [count, setCount] = useState(0); is typed as [number, (value: number) => void] under the hood — which is exactly why array destructuring always pulls the value out first and the setter function second, in that fixed order, no matter what you name them.
const point: number[] = [10, 20]; // allows point.push(30) — now 3 elements, still type-valid const point: [number, number] = [10, 20]; // push would be flagged, and a 3rd element assignment is an error
Typing fixed-shape data like coordinates as a plain array loses the length and position guarantees a tuple provides — a regular number[] happily accepts any number of numbers, silently allowing bugs like an accidental extra coordinate.
[multiple-choice] What is the key difference between an array type like number[] and a tuple type like [number, number]? A. There is no difference, they're identical B. A tuple fixes both the length and the type of each position; an array allows any length with one shared element type C. Tuples can only hold strings D. Arrays are always readonly, tuples are always mutable
B. A tuple fixes both the length and the type of each position; an array allows any length with one shared element type — A tuple type describes a fixed-length structure where each position has its own type, while a regular array type describes a list of any length where every element shares one common type.
[multiple-choice] Given const point: [number, number] = [10, 20];, what does TypeScript do if you try const bad: [number, number] = [10, 20, 30];? A. It compiles fine, extra elements are ignored B. It's a compile-time error because the tuple type requires exactly two elements C. It only errors at runtime D. It automatically truncates the array to two elements
B. It's a compile-time error because the tuple type requires exactly two elements — Tuple types are strict about length — supplying three elements where the tuple type declares exactly two produces a compile-time type error.
[multiple-choice] When should you give Array.prototype.reduce an explicit generic type parameter? A. Always, for every reduce call B. Never, TypeScript can always infer it C. When the accumulator's final shape differs from the array's element type D. Only when reducing an array of strings
C. When the accumulator's final shape differs from the array's element type — When the accumulator type matches the array's element type, inference handles it automatically; when it differs (e.g., building an object from an array of strings), supplying the generic type parameter explicitly tells TypeScript the accumulator's intended shape.
Define a tuple type RgbColor as [red: number, green: number, blue: number], write a function toHex(color: RgbColor): string that converts it to a hex string, and create a readonly variant frozenBlack: readonly [number, number, number] that cannot be mutated.
type RgbColor = [red: number, green: number, blue: number]; function toHex([r, g, b]: RgbColor): string { const channel = (n: number) => n.toString(16).padStart(2, "0"); return #${channel(r)}${channel(g)}${channel(b)}; } const frozenBlack: readonly [number, number, number] = [0, 0, 0]; console.log(toHex(frozenBlack as RgbColor)); // "#000000" // frozenBlack[0] = 255; // Error: index signature in type 'readonly [...]' is readonly
Use .toString(16).padStart(2, "0") on each channel and join the results; mark the frozen variant's type with a leading readonly.