Skip to main content
Advertisement

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.

SituationRecommended SyntaxReason
Simple types (string[])T[]More concise and readable
Complex generic typesArray<T>Nested angle brackets are less confusing
Union type elementsArray<string | number>Clearer than (string | number)[]
Readonly arraysreadonly 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

CategorySyntaxCharacteristicsPrimary Use
Basic arrayT[]Variable length, single typeGeneral collections
Generic arrayArray<T>Same, better readability for complex typesUnion/function type elements
Readonly arrayreadonly T[]Mutation methods unavailableImmutable data, pure function parameters
MultidimensionalT[][]Nested arraysMatrices, image data
Basic tuple[T1, T2]Fixed length, typed per indexuseState return values, key-value pairs
Labeled tuple[a: T1, b: T2]Improved readabilityMeaningful positional data
Optional tuple[T1, T2?]Last element can be omittedFixed structures with options
Rest tuple[T1, ...T2[]]Variable length + fixed front/backLog entries, function arguments
Const tuple[...] as constLocked to literal types, readonlyConstant 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.

Advertisement