Working with Arrays and Tuples


Working with Arrays and Tuples#
Arrays and tuples are fundamental to working with collections in TypeScript. Arrays let you work with dynamic lists of values, while tuples provide fixed-length, position-specific typing. Understanding both gives you the tools to model complex data structures with complete type safety.
Quick Distinction: Arrays are for homogeneous collections of any length
(string[]), while tuples are for heterogeneous collections of fixed length
([string, number]).
Array Types in Depth
Two Ways to Declare Arrays
TypeScript offers two syntaxes for array types—both are equivalent, so choose based on your preference and consistency:
// Syntax 1: Type[] (more common)
let numbers: number[] = [1, 2, 3, 4, 5];
let names: string[] = ["Alice", "Bob", "Charlie"];
let flags: boolean[] = [true, false, true];
// Syntax 2: Array<Type> (generic syntax)
let scores: Array<number> = [95, 87, 92];
let tags: Array<string> = ["typescript", "javascript"];
let results: Array<boolean> = [true, true, false];Both syntaxes work identically, but Type[] is generally preferred for simple types:
// Preferred for simple types
let ids: number[] = [1, 2, 3];
// Preferred when nested or complex
let matrix: Array<Array<number>> = [
[1, 2],
[3, 4],
];
// vs
let matrix: number[][] = [
[1, 2],
[3, 4],
]; // More readableConvention: Use Type[] for simple arrays and save Array<Type> for when you need generic constraints or complex nested structures.
Multi-Dimensional Arrays
TypeScript handles nested arrays with type safety at every level:
// 2D array (matrix)
let matrix: number[][] = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
];
// Access with type safety
let value: number = matrix[0][0]; // 1
let row: number[] = matrix[1]; // [4, 5, 6]
// 3D array
let cube: number[][][] = [
[
[1, 2],
[3, 4],
],
[
[5, 6],
[7, 8],
],
];
// Mixed types in nested arrays
let data: string[][] = [
["Name", "Age", "City"],
["Alice", "30", "NYC"],
["Bob", "25", "LA"],
];Practical example - Tic-tac-toe board:
type Cell = "X" | "O" | null;
type Board = Cell[][];
let gameBoard: Board = [
["X", "O", "X"],
[null, "X", "O"],
["O", null, "X"],
];
function checkCell(board: Board, row: number, col: number): Cell {
return board[row][col];
}Arrays with Union Types
Arrays can hold multiple types using unions:
// Array where each element can be string OR number
let mixed: (string | number)[] = [1, "two", 3, "four", 5];
mixed.push(6); // ✓ Works
mixed.push("seven"); // ✓ Works
mixed.push(true); // ✗ Error: Type 'boolean' is not assignable
// Working with mixed arrays
mixed.forEach((item) => {
if (typeof item === "string") {
console.log(item.toUpperCase());
} else {
console.log(item.toFixed(2));
}
});Common use case - API responses:
type ApiResponse = {
id: number;
data: string | number | boolean;
};
let responses: ApiResponse[] = [
{ id: 1, data: "success" },
{ id: 2, data: 42 },
{ id: 3, data: true },
];Union of array types (different from array of unions):
// This is different - the ENTIRE value is either a string[] OR a number[]
let arrayOrArray: string[] | number[] = ["a", "b", "c"];
arrayOrArray = [1, 2, 3]; // ✓ Can reassign to number[]
// Cannot mix within the same array
arrayOrArray = ["a", 1]; // ✗ Error: Type 'string' is not assignable to type 'number'Readonly Arrays
Readonly arrays prevent modifications, which is crucial for functional programming and preventing bugs.
Creating Immutable Arrays
Three ways to create readonly arrays:
// Method 1: readonly modifier with Type[]
let numbers: readonly number[] = [1, 2, 3];
// Method 2: ReadonlyArray<Type>
let names: ReadonlyArray<string> = ["Alice", "Bob"];
// Method 3: const assertion (makes everything readonly)
let colors = ["red", "green", "blue"] as const;Attempting mutations causes errors:
let nums: readonly number[] = [1, 2, 3];
nums[0] = 99; // ✗ Error: Index signature in type 'readonly number[]' only permits reading
nums.push(4); // ✗ Error: Property 'push' does not exist
nums.pop(); // ✗ Error: Property 'pop' does not exist
nums.sort(); // ✗ Error: Property 'sort' does not exist
// Reading is fine
let first = nums[0]; // ✓ Works
let length = nums.length; // ✓ WorksReadonly vs Const
Understanding the difference between const and readonly:
// const prevents reassignment of the variable
const nums = [1, 2, 3];
nums = [4, 5, 6]; // ✗ Error: Cannot assign to 'nums' because it is a constant
nums.push(4); // ✓ Works! Array itself is mutable
// readonly prevents modification of array contents
let readonlyNums: readonly number[] = [1, 2, 3];
readonlyNums = [4, 5, 6]; // ✓ Works! Can reassign the variable
readonlyNums.push(4); // ✗ Error: Property 'push' does not exist
// Combining both for maximum immutability
const immutable: readonly number[] = [1, 2, 3];
immutable = [4, 5, 6]; // ✗ Error: Cannot reassign
immutable.push(4); // ✗ Error: Cannot mutateWorking with Readonly Arrays
Non-mutating methods work fine with readonly arrays:
let numbers: readonly number[] = [1, 2, 3, 4, 5];
// These all work (they return new arrays)
let doubled = numbers.map((n) => n * 2); // ✓ [2, 4, 6, 8, 10]
let evens = numbers.filter((n) => n % 2 === 0); // ✓ [2, 4]
let sum = numbers.reduce((a, b) => a + b, 0); // ✓ 15
let sliced = numbers.slice(1, 3); // ✓ [2, 3]
// To modify, create a new array
let mutable = [...numbers]; // Create mutable copy
mutable.push(6); // Now we can mutateBest Practice: Default to readonly arrays in function parameters. This
prevents accidental mutations and makes your code more predictable.
Function parameter example:
// Good: Prevents the function from modifying the input
function sum(numbers: readonly number[]): number {
return numbers.reduce((acc, n) => acc + n, 0);
}
// The caller knows their array won't be modified
let myNumbers = [1, 2, 3];
let total = sum(myNumbers); // myNumbers is unchangedIntroduction to Tuples
What Are Tuples?
Tuples are arrays with a fixed length and specific types at each position:
// Tuple: [string, number]
let person: [string, number] = ["Alice", 30];
// Type at position 0 must be string
// Type at position 1 must be number
let name: string = person[0]; // ✓ "Alice"
let age: number = person[1]; // ✓ 30
// Wrong types cause errors
person = [30, "Alice"]; // ✗ Error: Type 'number' is not assignable to type 'string'
person = ["Alice"]; // ✗ Error: Type '[string]' is not assignable to type '[string, number]'Tuple vs Array
Understanding when to use each:
// Array: Homogeneous, any length
let scores: number[] = [95, 87, 92, 88];
scores.push(90); // Can add more
// Tuple: Heterogeneous, fixed length
let student: [string, number, boolean] = ["Alice", 95, true];
student.push(90); // ⚠️ Technically works but defeats the purpose of tuples
// Type safety at each position
student[0].toUpperCase(); // ✓ TypeScript knows this is a string
student[1].toFixed(2); // ✓ TypeScript knows this is a number
student[2] = false; // ✓ TypeScript knows this is a booleanWhen to use tuples:
- Fixed number of related values with different types
- Coordinate pairs
[number, number] - Function return values
[data, error] - Key-value pairs
[string, any]
When to use arrays:
- Dynamic number of values
- All values have the same type
- Need array methods like
push,pop, etc.
Tuple Patterns and Use Cases
Coordinates and Pairs
Tuples excel at representing coordinate systems:
// 2D coordinates
type Point2D = [number, number];
let origin: Point2D = [0, 0];
let point: Point2D = [10, 20];
function distance(p1: Point2D, p2: Point2D): number {
let [x1, y1] = p1;
let [x2, y2] = p2;
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}
// 3D coordinates
type Point3D = [number, number, number];
let position: Point3D = [10, 20, 30];
// RGB colors
type RGB = [number, number, number];
let red: RGB = [255, 0, 0];
let blue: RGB = [0, 0, 255];
function toHex([r, g, b]: RGB): string {
return `#${r.toString(16)}${g.toString(16)}${b.toString(16)}`;
}Function Return Values
Tuples are perfect for returning multiple values:
// Multiple return values
function getUser(id: number): [string, number, string] {
// Fetch user from database
return ["Alice", 30, "[email protected]"];
}
let [name, age, email] = getUser(1);
// Error handling pattern (like Go or Rust)
function divide(a: number, b: number): [number, string | null] {
if (b === 0) {
return [0, "Cannot divide by zero"];
}
return [a / b, null];
}
let [result, error] = divide(10, 2);
if (error) {
console.error(error);
} else {
console.log(`Result: ${result}`);
}
// React useState pattern
function useState<T>(initial: T): [T, (value: T) => void] {
let state = initial;
function setState(value: T) {
state = value;
}
return [state, setState];
}
let [count, setCount] = useState(0);Named Tuples
TypeScript 4.0+ supports labeled tuple elements for better readability:
// Without labels
type OldPoint = [number, number];
// With labels (better documentation)
type Point = [x: number, y: number];
type User = [name: string, age: number, email: string];
// Labels appear in IDE tooltips and improve readability
function createUser(): User {
return ["Alice", 30, "[email protected]"];
}
// Labels don't affect usage
let user: User = ["Bob", 25, "[email protected]"];
let [name, age, email] = user; // Destructuring works the sameIDE Benefit: Named tuples show parameter names in autocomplete, making your code self-documenting without requiring separate documentation.
Optional Tuple Elements
Make tuple elements optional with ?:
// Optional third element
type Response = [status: number, data: string, error?: string];
let success: Response = [200, "Success"];
let failure: Response = [404, "Not Found", "Page does not exist"];
// Optional elements must come after required ones
type Config = [host: string, port?: number, ssl?: boolean];
let config1: Config = ["localhost"];
let config2: Config = ["localhost", 3000];
let config3: Config = ["localhost", 3000, true];Rest Elements in Tuples
Tuples can have rest elements for variable-length endings:
// First two elements are required, rest are numbers
type NumberList = [first: number, second: number, ...rest: number[]];
let nums1: NumberList = [1, 2];
let nums2: NumberList = [1, 2, 3, 4, 5];
// String followed by any number of numbers
type LogEntry = [message: string, ...data: number[]];
let log1: LogEntry = ["Error"];
let log2: LogEntry = ["Temperature reading", 98.6, 99.1, 97.8];
// Mixed types in rest
type MixedTuple = [id: number, ...values: (string | number)[]];
let mixed: MixedTuple = [1, "hello", 42, "world", 99];Readonly Tuples
Make tuples immutable with readonly:
// Readonly tuple
let point: readonly [number, number] = [10, 20];
point[0] = 15; // ✗ Error: Cannot assign to '0' because it is a read-only property
point.push(30); // ✗ Error: Property 'push' does not exist
// const assertion creates readonly tuple
let rgb = [255, 0, 0] as const;
// Type: readonly [255, 0, 0]
// Function with readonly tuple parameter
function draw(point: readonly [number, number]) {
let [x, y] = point; // Reading is fine
// point[0] = 5; // Would error
}Practical example:
// Configuration tuple that shouldn't change
const API_CONFIG = ["https://api.example.com", 443, true] as const;
// Type: readonly ["https://api.example.com", 443, true]
function connectToAPI(config: readonly [string, number, boolean]) {
let [url, port, useSSL] = config;
// Connection logic
}
connectToAPI(API_CONFIG);Array and Tuple Methods
Type-Safe Array Methods
TypeScript provides full type safety with array methods:
let numbers: number[] = [1, 2, 3, 4, 5];
// Map maintains type
let doubled: number[] = numbers.map((n) => n * 2);
let strings: string[] = numbers.map((n) => `Number: ${n}`);
// Filter maintains type
let evens: number[] = numbers.filter((n) => n % 2 === 0);
// Reduce infers return type
let sum: number = numbers.reduce((acc, n) => acc + n, 0);
// Find returns type | undefined
let found: number | undefined = numbers.find((n) => n > 3);
// Type narrowing in callbacks
let mixed: (string | number)[] = [1, "two", 3, "four"];
let onlyNumbers = mixed.filter(
(item): item is number => typeof item === "number"
);
// Type: number[]Destructuring with Type Safety
Destructure arrays and tuples with maintained types:
// Array destructuring
let numbers: number[] = [1, 2, 3, 4, 5];
let [first, second, ...rest] = numbers;
// first: number, second: number, rest: number[]
// Tuple destructuring
let user: [string, number, boolean] = ["Alice", 30, true];
let [name, age, isActive] = user;
// name: string, age: number, isActive: boolean
// Skipping elements
let colors: [string, string, string] = ["red", "green", "blue"];
let [primary, , tertiary] = colors;
// primary: "red", tertiary: "blue"
// Default values
let incomplete: [string, number?] = ["Bob"];
let [userName, userAge = 0] = incomplete;
// userName: string, userAge: number
// Nested destructuring
let matrix: number[][] = [
[1, 2],
[3, 4],
];
let [[a, b], [c, d]] = matrix;
// a: number, b: number, c: number, d: numberPractical Examples
API Response Handling
Handle API responses with tuples for error handling:
type ApiResult<T> = [data: T | null, error: string | null];
async function fetchUser(id: number): Promise<ApiResult<User>> {
try {
let response = await fetch(`/api/users/${id}`);
let data = await response.json();
return [data, null];
} catch (error) {
return [null, error.message];
}
}
// Usage
let [user, error] = await fetchUser(1);
if (error) {
console.error("Failed to fetch user:", error);
} else {
console.log("User:", user.name);
}State Management
Model state updates with tuples:
type State = {
count: number;
user: string | null;
};
type Action = ["increment"] | ["decrement"] | ["setUser", string] | ["reset"];
function reducer(state: State, action: Action): State {
switch (action[0]) {
case "increment":
return { ...state, count: state.count + 1 };
case "decrement":
return { ...state, count: state.count - 1 };
case "setUser":
return { ...state, user: action[1] };
case "reset":
return { count: 0, user: null };
}
}
// Usage
let state: State = { count: 0, user: null };
state = reducer(state, ["increment"]);
state = reducer(state, ["setUser", "Alice"]);CSV and Table Data
Model tabular data with arrays of tuples:
// CSV data as array of tuples
type CSVRow = [id: number, name: string, age: number, city: string];
type CSVData = CSVRow[];
let users: CSVData = [
[1, "Alice", 30, "NYC"],
[2, "Bob", 25, "LA"],
[3, "Charlie", 35, "Chicago"],
];
function findUserById(data: CSVData, id: number): CSVRow | undefined {
return data.find(([rowId]) => rowId === id);
}
function getUserNames(data: CSVData): string[] {
return data.map(([, name]) => name);
}
// Type-safe column access
let [id, name, age, city] = users[0];Event Handling
Model events with discriminated tuples:
type MouseEvent = ["click", x: number, y: number];
type KeyEvent = ["keypress", key: string];
type ScrollEvent = ["scroll", scrollY: number];
type UIEvent = MouseEvent | KeyEvent | ScrollEvent;
function handleEvent(event: UIEvent) {
switch (event[0]) {
case "click":
let [, x, y] = event;
console.log(`Clicked at (${x}, ${y})`);
break;
case "keypress":
let [, key] = event;
console.log(`Key pressed: ${key}`);
break;
case "scroll":
let [, scrollY] = event;
console.log(`Scrolled to: ${scrollY}`);
break;
}
}Common Patterns and Best Practices
1. Use readonly by default for function parameters:
// ✓ Good
function processItems(items: readonly string[]) {
return items.map((item) => item.toUpperCase());
}
// ✗ Avoid (allows mutation)
function processItems(items: string[]) {
items.sort(); // Might be unintended side effect
return items;
}2. Prefer tuples for fixed, heterogeneous data:
// ✓ Good - fixed structure, different types
type Coordinate = [number, number];
// ✗ Avoid - use object instead
type Coordinate = { x: number; y: number }; // Better for this case3. Use named tuples for clarity:
// ✓ Good
type Range = [start: number, end: number];
// ✗ Less clear
type Range = [number, number];4. Combine const assertion with tuples:
// ✓ Good - readonly and literal types
const POSITION = [0, 0] as const;
// Type: readonly [0, 0]
// Mutable and wider type
const POSITION = [0, 0];
// Type: number[]5. Use array methods instead of loops:
// ✓ Good - type-safe and functional
let doubled = numbers.map((n) => n * 2);
// ✗ Avoid - more error-prone
let doubled: number[] = [];
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}Common Pitfalls
Pitfall 1: Tuple push/pop breaks type safety
let tuple: [string, number] = ["Alice", 30];
tuple.push("extra"); // ⚠️ Compiles but breaks tuple contract
// Type is still [string, number] but runtime has 3 elementsSolution: Use readonly tuples:
let tuple: readonly [string, number] = ["Alice", 30];
tuple.push("extra"); // ✗ Error: Property 'push' does not existPitfall 2: Array inference loses tuple type
let point = [10, 20]; // Type: number[] (not tuple!)
// Fix with type annotation
let point: [number, number] = [10, 20];
// Or const assertion
let point = [10, 20] as const; // Type: readonly [10, 20]Pitfall 3: Forgetting readonly for function params
function sum(nums: number[]) {
nums.sort(); // Mutates caller's array!
return nums.reduce((a, b) => a + b, 0);
}
// Fix
function sum(nums: readonly number[]) {
let sorted = [...nums].sort(); // Create copy
return sorted.reduce((a, b) => a + b, 0);
}Pitfall 4: Wrong destructuring variable count
let tuple: [string, number] = ["Alice", 30];
let [name, age, extra] = tuple; // extra is undefined but no error
// TypeScript doesn't error on extra destructured variables
// Be careful to match the tuple lengthNext Steps
You now have a solid understanding of arrays and tuples in TypeScript! Here's what to explore next:
Practice These Concepts:
- Refactor existing code to use readonly arrays
- Identify places where tuples would improve your code
- Experiment with named tuples in your APIs
- Use tuple destructuring in function parameters
Continue Learning:
- Object types and interfaces
- Enums and their alternatives
- Advanced type manipulation
- Generic types with arrays
Congratulations! You've mastered arrays and tuples in TypeScript. These collection types are essential for modeling data in type-safe ways. Practice using them in real projects to solidify your understanding.
Questions about arrays or tuples? Having trouble deciding between arrays and tuples? Drop a comment below and let's discuss!