Skip to main content

TS Ep 5: Arrays, Tuples, and Readonly Collections

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 5: This Article
Collections of data are everywhere in application logic. TypeScript provides rich type structures for both variable-length homogeneous collections (Arrays) and fixed-length heterogeneous records (Tuples).

1. Array Types: T[] vs Array<T>
#

In TypeScript, there are two equivalent syntaxes for typing arrays:

  1. Bracket Notation (T[]): The standard, idiomatic syntax used across most codebases.
  2. Generic Notation (Array<T>): Uses TypeScript’s built-in Array interface.
// Bracket notation (Idiomatic)
const names: string[] = ["Alice", "Bob", "Charlie"];

// Generic notation (Equivalent)
const scores: Array<number> = [95, 88, 100];

Complex Array Elements
#

If an array contains objects, union types, or nested arrays:

interface Product {
  id: string;
  price: number;
}

// Array of custom objects
const inventory: Product[] = [
  { id: "prod_1", price: 19.99 },
  { id: "prod_2", price: 49.99 },
];

2. Union Arrays & Operator Precedence
#

A common pitfall is confusing (string | number)[] with string | number[].

// 🟢 Correct: An array where elements can be EITHER a string OR a number
const mixedValues: (string | number)[] = ["hello", 42, "world", 100];

// ❌ Incorrect Precedence: EITHER a single string OR an array of numbers!
let wrongUnion: string | number[];
wrongUnion = "Just a string"; // Valid
wrongUnion = [10, 20, 30];    // Valid
// wrongUnion = ["hello", 10]; // Error! Cannot mix strings and numbers in array.
Type SyntaxMeaningExample Valid Value
(string | number)[]Array containing strings and/or numbers["a", 1, "b", 2]
string | number[]Either a string primitive OR a number array"hello" or [1, 2]
Array<string | number>Equivalent to (string | number)[][1, "a"]

3. Tuples: Fixed-Length & Specific Types
#

While arrays have an unknown length and hold homogeneous data, a Tuple is an array with a fixed length and specific, known types at each index position.

// Tuple representing a 3D Coordinate: [x, y, z]
const position: [number, number, number] = [10.5, 20.0, 0.0];

// Tuple representing an HTTP Response: [statusCode, message]
const response: [number, string] = [200, "OK"];

// ❌ Compiler Error: Type 'string' is not assignable to type 'number' at index 0
// const invalidResponse: [number, string] = ["OK", 200];

Labeled Tuples (Self-Documenting Tuples)
#

TypeScript allows adding optional labels to tuple elements. Labels do not change the runtime behavior; they serve purely as developer documentation for IDE tooltips:

// Labeled Tuple
type GeoPoint = [latitude: number, longitude: number];
type HttpResponse = [status: number, body: string, headers?: Record<string, string>];

const point: GeoPoint = [-6.2088, 106.8456];

When calling a function that uses a labeled tuple, your editor highlights latitude: number and longitude: number in the parameter signature.

Rest Elements in Tuples
#

Tuples can use the rest operator (...) to define minimum fixed elements followed by an arbitrary number of trailing elements:

// Must start with a string (command name), followed by 0 or more numbers (arguments)
type CLICommand = [command: string, ...args: number[]];

const cmd1: CLICommand = ["set-volume", 80];
const cmd2: CLICommand = ["sum", 10, 20, 30, 40];
// ❌ Error: Index 0 MUST be a string!
// const cmd3: CLICommand = [10, 20]; 

4. Readonly Arrays and Tuples (readonly)
#

By default, JavaScript arrays are mutable objects. Array methods like .push(), .pop(), .splice(), and .sort() mutate the original array in place.

To enforce immutability at compile time, use readonly:

// Readonly Array
const immutableTags: readonly string[] = ["typescript", "javascript"];

// ❌ Compiler Error: Property 'push' does not exist on type 'readonly string[]'.
// immutableTags.push("python");

// ❌ Compiler Error: Index signature in type 'readonly string[]' only permits reading.
// immutableTags[0] = "go";

Readonly Syntax Variations
#

// Equivalent Readonly Array Syntaxes:
const arr1: readonly string[] = ["a", "b"];
const arr2: ReadonlyArray<string> = ["a", "b"];

// Readonly Tuple:
const point: readonly [number, number] = [10, 20];
// point[0] = 30; // Error!
Caution

The Historical Tuple Mutation Gotcha: In older TypeScript versions, calling .push() on a non-readonly tuple [number, string] did not throw a compile error due to a historical trade-off in the compiler. Always mark tuples as readonly if you do not intend for them to be mutated!


Code Example: Function Returning a Tuple (React useState Pattern)
#

// Custom tuple-returning function modeled after React hooks
function createSimpleState<T>(initialValue: T): readonly [() => T, (newValue: T) => void] {
  let state = initialValue;

  const getter = () => state;
  const setter = (newValue: T) => {
    state = newValue;
    console.log(`State updated to: ${state}`);
  };

  return [getter, setter] as const;
}

const [getScore, setScore] = createSimpleState(100);

console.log(getScore()); // 100
setScore(150);           // "State updated to: 150"
console.log(getScore()); // 150

Summary & Next Steps
#

In this episode:

  • We compared bracket array syntax T[] with generic syntax Array<T>.
  • We resolved union array precedence: (string | number)[] vs string | number[].
  • We built fixed-length Tuples, Labeled Tuples, and Rest Tuples.
  • We secured collection immutability using readonly T[] and readonly [T, U].

In Episode 6: Any vs. Unknown, we will analyze TypeScript’s top types: any (the type-safety dynamic escape hatch) and unknown (the type-safe top type)!

typescript - This article is part of a series.
Part 5: This Article