2.3 Arrays and Tuples
An array is an ordered collection of values of the same type. A tuple is a special array with a fixed length where each index has a defined type. TypeScript provides strong type checking for both structures and maximizes expressiveness with features like the readonly modifier, labeled tuples, as const assertions, and rest elements.
Array Type Declaration — T[] vs Array<T>
There are two ways to express an array's type. The result is completely identical.
T[] Syntax
const numbers: number[] = [1, 2, 3, 4, 5];
const names: string[] = ["Alice", "Bob", "Charlie"];
const flags: boolean[] = [true, false, true];
Array<T> Generic Syntax
const numbers: Array<number> = [1, 2, 3, 4, 5];
const names: Array<string> = ["Alice", "Bob", "Charlie"];
Which One Should You Choose?
Both syntaxes produce identical compiled output, but the choice depends on the situation.
| Situation | Recommended Syntax | Reason |
|---|---|---|
Simple types (string[]) | T[] | More concise and readable |
| Complex generic types | Array<T> | Nested angle brackets are less confusing |
| Union type elements | Array<string | number> | Clearer than (string | number)[] |
| Readonly arrays | readonly T[] or ReadonlyArray<T> | Both are identical |
// Union type elements — Array<T> is more readable
const mixed: Array<string | number> = ["a", 1, "b", 2];
const mixed2: (string | number)[] = ["a", 1, "b", 2]; // parentheses required
// Complex nested types — Array<T> is clearer
type Callback = (value: number) => void;
const callbacks: Array<Callback> = [];
const callbacks2: ((value: number) => void)[] = []; // harder to read
Array Methods and Type Safety
TypeScript also accurately infers the return types of array methods.
const scores: number[] = [90, 85, 78, 92, 88];
const doubled = scores.map((s) => s * 2); // number[]
const passed = scores.filter((s) => s >= 80); // number[]
const total = scores.reduce((sum, s) => sum + s, 0); // number
const first = scores.find((s) => s > 90); // number | undefined
// Wrong callback — type error
const wrong = scores.map((s) => s.toUpperCase());
// error: Property 'toUpperCase' does not exist on type 'number'
readonly Arrays — Guaranteeing Immutability
Adding the readonly modifier prevents calling methods that mutate the array's contents (push, pop, splice, sort, reverse, etc.). This is useful for expressing in types that a function will not modify a passed-in array.
const frozen: readonly number[] = [1, 2, 3];
frozen.push(4); // error: Property 'push' does not exist on type 'readonly number[]'
frozen.pop(); // error
frozen[0] = 99; // error: Index signature in type 'readonly number[]' only permits reading
// Reading is allowed
console.log(frozen[0]); // 1
console.log(frozen.length); // 3
console.log(frozen.map((n) => n * 2)); // [2, 4, 6] — OK because map returns a new array
ReadonlyArray<T>
Completely identical to readonly T[]. May provide slightly better readability in long type expressions.
function processItems(items: ReadonlyArray<string>): void {
// explicitly states the intent to only read items
items.forEach((item) => console.log(item));
// items.push("new"); // error
}
The Relationship Between Regular Arrays and readonly Arrays
Because readonly arrays are more restrictive than regular arrays, a regular array can be assigned to a readonly array, but not the other way around.
const mutable: number[] = [1, 2, 3];
const immutable: readonly number[] = mutable; // OK — widening to a more restrictive type
// Assigning immutable back to mutable is not allowed
const mutable2: number[] = immutable;
// error: Type 'readonly number[]' is not assignable to type 'number[]'
// 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'
Applying readonly to Function Parameters
Expresses in types the contract that a function will not mutate the array.
// A pure function that does not mutate the input array
function sum(arr: readonly number[]): number {
return arr.reduce((total, n) => total + n, 0);
}
function sortedCopy(arr: readonly string[]): string[] {
return [...arr].sort(); // create a new array via spread, then sort
}
const original = ["banana", "apple", "cherry"];
const sorted = sortedCopy(original);
console.log(original); // ["banana", "apple", "cherry"] — unchanged
console.log(sorted); // ["apple", "banana", "cherry"]
Multidimensional Arrays
How to declare nested array types.
// 2D array — matrix
const matrix: number[][] = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
];
// Or Array<Array<number>>
const matrix2: Array<Array<number>> = [[1, 2], [3, 4]];
// 3D array
const cube: number[][][] = [
[[1, 2], [3, 4]],
[[5, 6], [7, 8]],
];
Multidimensional Arrays in Practice
// Chess board — 8x8
type Piece = "K" | "Q" | "R" | "B" | "N" | "P" | null;
type Board = Piece[][];
function createEmptyBoard(): Board {
return Array.from({ length: 8 }, () => Array(8).fill(null) as Piece[]);
}
function getCell(board: Board, row: number, col: number): Piece {
if (row < 0 || row >= 8 || col < 0 || col >= 8) {
throw new RangeError(`Invalid coordinate: (${row}, ${col})`);
}
return board[row][col];
}
// Image pixel data — [R, G, B, A][]
type Pixel = [number, number, number, number]; // strongly typed with a tuple
type ImageData = Pixel[][];
Tuples
A tuple is an array where the type at each index is fixed. Its length is also fixed. It is useful when dealing with data where position carries meaning.
// Fixed shape: [id, name, isActive]
let user: [number, string, boolean] = [1, "Alice", true];
// Type-safe access by index
const id: number = user[0];
const name: string = user[1];
const isActive: boolean = user[2];
// Destructuring assignment
const [userId, userName, userActive] = user;
console.log(`${userId}: ${userName} (${userActive ? "active" : "inactive"})`);
// Wrong assignment — error
user = ["Alice", 1, true];
// error: Type 'string' is not assignable to type 'number'
Labeled Tuples (TypeScript 4.0+)
Naming each element improves readability. Labels do not affect type checking, but they appear in IDE tooltips and error messages.
// Tuple without labels
type Point2D = [number, number];
// Tuple with labels — meaning is clear
type Point2DLabeled = [x: number, y: number];
type Point3DLabeled = [x: number, y: number, z: number];
// Applying labels to function return value tuples
type MinMax = [min: number, max: number];
function getRange(arr: readonly number[]): MinMax {
return [Math.min(...arr), Math.max(...arr)];
}
const [min, max] = getRange([3, 1, 4, 1, 5, 9, 2, 6]);
console.log(`Range: ${min} ~ ${max}`); // Range: 1 ~ 9
Optional Tuple Elements
Mark optional elements with ?. Optional elements must be at the end.
type RGB = [r: number, g: number, b: number];
type RGBA = [r: number, g: number, b: number, a?: number];
const red: RGBA = [255, 0, 0]; // a omitted — OK
const semiRed: RGBA = [255, 0, 0, 0.5]; // a included — OK
// The optional element type is number | undefined
function toCSS(color: RGBA): string {
const [r, g, b, a] = color;
if (a !== undefined) {
return `rgba(${r}, ${g}, ${b}, ${a})`;
}
return `rgb(${r}, ${g}, ${b})`;
}
Rest Element Tuples — Variable Length
You can mix fixed and variable elements using the spread operator (...).
// First is string, rest is number[]
type StringThenNumbers = [first: string, ...rest: number[]];
const a: StringThenNumbers = ["score"]; // no rest — OK
const b: StringThenNumbers = ["score", 10]; // one rest — OK
const c: StringThenNumbers = ["score", 10, 20, 30]; // multiple rest — OK
// Last is boolean, preceding elements are variable number[]
type NumbersThenBoolean = [...nums: number[], flag: boolean];
const d: NumbersThenBoolean = [true]; // no nums
const e: NumbersThenBoolean = [1, 2, 3, true]; // three nums
// Fixed on both ends + variable in the middle
type Sandwich = [bread: string, ...fillings: string[], bread2: string];
const sandwich: Sandwich = ["sourdough", "ham", "cheese", "baguette"];
Rest Element Tuples in Practice
// Expressing function signature types
type FunctionArgs = [callback: (...args: unknown[]) => void, delay: number, ...params: unknown[]];
// Event log type
type LogEntry = [timestamp: number, level: "info" | "warn" | "error", ...messages: string[]];
function createLog(level: "info" | "warn" | "error", ...messages: string[]): LogEntry {
return [Date.now(), level, ...messages];
}
const entry = createLog("warn", "Disk usage at 90%", "Immediate attention required");
const [ts, lvl, ...msgs] = entry;
console.log(`[${lvl}] ${msgs.join(" ")}`); // [warn] Disk usage at 90% Immediate attention required
as const with Tuples — Preserving Literal Types
Using as const locks the types of an array or tuple to literal types. This is particularly powerful with configuration objects or constant arrays.
// Without as const
const point = [10, 20];
// inferred type: number[] — elements widened to number
// With as const
const pointConst = [10, 20] as const;
// inferred type: readonly [10, 20] — locked as a literal type tuple
// as const applies deeply
const config = {
endpoints: ["api/users", "api/posts"] as const,
timeout: 5000 as const,
};
// config.endpoints type: readonly ["api/users", "api/posts"]
// config.timeout type: 5000
// Cannot be mutated
pointConst[0] = 99; // error: Cannot assign to '0' because it is a read-only property
Forcing Function Return Values to be Tuples with as const
When a function returns an array, TypeScript infers it as T[] by default. To make the return value a tuple, you need as const or an explicit return type.
// Problem: inferred as array
function useCounterBad() {
let count = 0;
const increment = () => { count++; };
return [count, increment];
// type: (number | (() => void))[] — cannot distinguish type by index
}
// Solution 1: explicitly declare return type
function useCounterTyped(): [count: number, increment: () => void] {
let count = 0;
const increment = () => { count++; };
return [count, increment];
}
// Solution 2: as const (note: readonly is added)
function useCounterConst() {
let count = 0;
const increment = () => { count++; };
return [count, increment] as const;
// type: readonly [number, () => void]
}
const [count, increment] = useCounterTyped();
// count: number, increment: () => void — clearly distinguished
Practical Example 1: React useState Return Value Type
// Implementing React's useState type signature directly helps you understand the meaning of tuples
type SetState<T> = (newValue: T | ((prev: T) => T)) => void;
type UseState<T> = [state: T, setState: SetState<T>];
function useState<T>(initialValue: T): UseState<T> {
let state = initialValue;
const setState: SetState<T> = (newValue) => {
if (typeof newValue === "function") {
state = (newValue as (prev: T) => T)(state);
} else {
state = newValue;
}
};
return [state, setState];
}
// Usage
const [count, setCount] = useState(0);
// count: number, setCount: SetState<number>
const [name, setName] = useState("Alice");
// name: string, setName: SetState<string>
setCount(1);
setCount((prev) => prev + 1);
Practical Example 2: CSV Parsing
// Type-safely parsing each row of a CSV
type CsvRow = [id: string, name: string, score: number, passed: boolean];
function parseCsvRow(line: string): CsvRow {
const parts = line.split(",").map((s) => s.trim());
if (parts.length !== 4) {
throw new Error(`Invalid CSV row: ${line}`);
}
const [id, name, scoreStr, passedStr] = parts;
const score = Number(scoreStr);
if (Number.isNaN(score)) {
throw new Error(`Score is not a number: ${scoreStr}`);
}
return [id, name, score, passedStr.toLowerCase() === "true"];
}
function parseCsv(csv: string): CsvRow[] {
return csv
.split("\n")
.slice(1) // skip header
.filter((line) => line.trim().length > 0)
.map(parseCsvRow);
}
const sampleCsv = `id,name,score,passed
1,Alice,95,true
2,Bob,72,false
3,Charlie,88,true`;
const rows = parseCsv(sampleCsv);
rows.forEach(([id, name, score, passed]) => {
console.log(`${id}. ${name}: ${score} (${passed ? "Pass" : "Fail"})`);
});
Practical Example 3: Coordinate System
// 2D, 3D, 4D (homogeneous coordinates)
type Point2D = [x: number, y: number];
type Point3D = [x: number, y: number, z: number];
type Point4D = [x: number, y: number, z: number, w: number];
function distance2D([x1, y1]: Point2D, [x2, y2]: Point2D): number {
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}
function distance3D([x1, y1, z1]: Point3D, [x2, y2, z2]: Point3D): number {
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2 + (z2 - z1) ** 2);
}
function translate3D([x, y, z]: Point3D, [dx, dy, dz]: Point3D): Point3D {
return [x + dx, y + dy, z + dz];
}
function scale3D([x, y, z]: Point3D, factor: number): Point3D {
return [x * factor, y * factor, z * factor];
}
// Path calculation
const waypoints: Point3D[] = [
[0, 0, 0],
[3, 4, 0],
[3, 4, 5],
];
function pathLength(points: readonly Point3D[]): number {
let total = 0;
for (let i = 1; i < points.length; i++) {
total += distance3D(points[i - 1], points[i]);
}
return total;
}
console.log(`Path length: ${pathLength(waypoints)}`); // 10
Pro Tips
Tip 1: Enforce array immutability at function boundaries
// Mutable internally, exposed as readonly externally
class DataStore {
private _items: string[] = [];
get items(): readonly string[] {
return this._items; // return a readonly view
}
add(item: string): void {
this._items.push(item); // mutable internally
}
}
Tip 2: Separate arrays with rest elements during destructuring
const [first, second, ...rest] = [1, 2, 3, 4, 5];
// first: 1, second: 2, rest: number[]
// Separate head and tail
const arr = [1, 2, 3, 4, 5];
const [head, ...tail] = arr; // head: 1, tail: [2, 3, 4, 5]
Tip 3: Work with tuple types using utility types
// Extract the type of the first element of a tuple
type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never;
type First = Head<[string, number, boolean]>; // string
// Extract the types of the remaining elements of a tuple
type Tail<T extends unknown[]> = T extends [unknown, ...infer R] ? R : never;
type Rest = Tail<[string, number, boolean]>; // [number, boolean]
Tip 4: Convert as const arrays to union types
const STATUSES = ["pending", "active", "inactive", "banned"] as const;
type Status = typeof STATUSES[number];
// Status = "pending" | "active" | "inactive" | "banned"
// The array can also be used at runtime — a pattern frequently used instead of Enum
function isValidStatus(value: string): value is Status {
return (STATUSES as readonly string[]).includes(value);
}
Tip 5: When an object is better than a tuple
Tuples work well when there are few elements (2–3) and positions are clear. Beyond that, a named object type is better.
// Bad: many elements mean you have to memorize what each index means
type UserTuple = [number, string, string, boolean, string, number];
// Good: named properties convey meaning
interface UserRecord {
id: number;
firstName: string;
lastName: string;
isActive: boolean;
email: string;
age: number;
}
Summary Table
| Category | Syntax | Characteristics | Primary Use |
|---|---|---|---|
| Basic array | T[] | Variable length, single type | General collections |
| Generic array | Array<T> | Same, better readability for complex types | Union/function type elements |
| Readonly array | readonly T[] | Mutation methods unavailable | Immutable data, pure function parameters |
| Multidimensional | T[][] | Nested arrays | Matrices, image data |
| Basic tuple | [T1, T2] | Fixed length, typed per index | useState return values, key-value pairs |
| Labeled tuple | [a: T1, b: T2] | Improved readability | Meaningful positional data |
| Optional tuple | [T1, T2?] | Last element can be omitted | Fixed structures with options |
| Rest tuple | [T1, ...T2[]] | Variable length + fixed front/back | Log entries, function arguments |
| Const tuple | [...] as const | Locked to literal types, readonly | Constant configs, union sources |
The next chapter covers enums. We will look at the differences between numeric, string, and const enums, the tree-shaking problem, and the modern as const object alternative pattern that can be applied directly in real-world code.