Logo

Home

About

Blog

Contact

Guestbook

Portfolio

Privacy

TOS

Click to navigate

  1. Home
  2. Joshua R. Lehman's Blog
  3. Working with Arrays and Tuples

Table of contents

  • Share on X
  • Discuss on X

Related Articles

Basic Types in TypeScript
TypeScript
9m
Nov 23, 2025

Basic Types in TypeScript

TypeScript's type system starts with fundamental building blocks: primitive types like string, number, and boolean, along with arrays and type annotations. Understanding these basic types is essential for writing type-safe code. This comprehensive guide walks you through each primitive type, shows you how to use type annotations effectively, and teaches you best practices for working with arrays and basic data structures in TypeScript.

#TypeScript#Types+5
Introduction to TypeScript: Why Static Typing Matters
TypeScript
7m
Nov 6, 2025

Introduction to TypeScript: Why Static Typing Matters

TypeScript has evolved from a Microsoft experiment to the industry standard for building scalable JavaScript applications. Understanding why static typing matters is the first step in mastering modern web development. This comprehensive introduction explores the benefits, use cases, and real-world impact of adopting TypeScript.

#TypeScript#JavaScript+6
Understanding the TypeScript Compiler
TypeScript
6m
Nov 18, 2025

Understanding the TypeScript Compiler

The TypeScript compiler (tsc) is the engine that transforms your TypeScript code into JavaScript. Understanding how it works, what happens during compilation, and how to leverage features like watch mode will make you a more effective TypeScript developer. This guide demystifies the compilation process and shows you how to optimize your workflow with the TypeScript compiler.

#TypeScript#Compiler+5

© Joshua R. Lehman

Full Stack Developer

Crafted with passion • Built with modern web technologies

2025 • All rights reserved

Contents

  • Array Types in Depth
    • Two Ways to Declare Arrays
    • Multi-Dimensional Arrays
    • Arrays with Union Types
  • Readonly Arrays
    • Creating Immutable Arrays
    • Readonly vs Const
    • Working with Readonly Arrays
  • Introduction to Tuples
    • What Are Tuples?
    • Tuple vs Array
  • Tuple Patterns and Use Cases
    • Coordinates and Pairs
    • Function Return Values
    • Named Tuples
    • Optional Tuple Elements
    • Rest Elements in Tuples
  • Readonly Tuples
  • Array and Tuple Methods
    • Type-Safe Array Methods
    • Destructuring with Type Safety
  • Practical Examples
    • API Response Handling
    • State Management
    • CSV and Table Data
    • Event Handling
  • Common Patterns and Best Practices
  • Common Pitfalls
  • Next Steps
TypeScript

Working with Arrays and Tuples

November 28, 2025•14 min read
Joshua R. Lehman
Joshua R. Lehman
Author
TypeScript arrays and tuples visualization
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 readable

Convention: 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; // ✓ Works

Readonly 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 mutate

Working 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 mutate

Best 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 unchanged

Introduction 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 boolean

When 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 same

IDE 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: number

Practical 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 case

3. 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 elements

Solution: Use readonly tuples:

let tuple: readonly [string, number] = ["Alice", 30];
tuple.push("extra"); // ✗ Error: Property 'push' does not exist

Pitfall 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 length

Next 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!