Behavioral Pattern (GoF)

Iterator

Provides a way to sequentially access the elements of a collection without exposing its internal representation — decoupling the traversal algorithm from the underlying data structure.

Intent

Encapsulate a collection's traversal mechanism in a separate object (the Iterator), exposing only a sequential-access interface — hasNext() / next() in the GoF style, or the languages' native protocols (Symbol.iterator and generators in TypeScript; Iterator/IteratorAggregate and generators in PHP). The client walks through the collection without knowing whether it's an array, a linked list, a tree, or a database result set.

Cataloged by the GoF (1994) as a behavioral pattern, Iterator is today so fundamental that every modern language has baked it into its native iteration protocols. In TypeScript, any object that implements Symbol.iterator can be used in for...of, spread ([...col]), and destructuring. In PHP, implementing Iterator or IteratorAggregate enables the native foreach.

Problem

A music app stores the playlist in a custom linked list. The display UI needs to walk through the list to show the tracks. The direct approach exposes the internal structure:

// Naive approach — DON'T do this:
class PlaylistUI {
  show(playlist: LinkedList): void {
    // Direct coupling to the internal structure — accesses the node directly.
    let node = playlist.head;   // exposed internal property
    while (node !== null) {
      console.log(node.value);  // accesses the node's internal field
      node = node.next;         // walks the internal structure
    }
  }
}

If LinkedList changes to a circular array or a balanced tree, all the traversal logic in the UI needs to be rewritten. On top of that, implementing a second form of traversal (e.g.: reverse order) requires duplicating the loop somewhere else — with no reusable abstraction.

Iterator extracts the traversal into a dedicated object. The collection exposes only a createIterator() method — the client uses the Iterator's interface without knowing anything about the collection's internal structure.

Solution

Iterator organizes the code into four participants:

  1. Iterator (interface): declares the traversal operations. GoF style: hasNext(): boolean and next(): T. In PHP's SPL: current(), next(), rewind(), valid(), key(). In TypeScript: the method [Symbol.iterator]() returning an object with next(): IteratorResult<T>.
  2. ConcreteIterator: keeps the internal cursor over the collection and implements the traversal operations. Each call to createIterator() returns a new instance with its own cursor — allowing multiple independent simultaneous traversals.
  3. Aggregate (interface): declares the iterator creation method. E.g.: createIterator(): ListIterator<T>.
  4. ConcreteAggregate: the concrete collection that implements the Aggregate and returns the appropriate ConcreteIterator. It can provide multiple iterator types (forward, reverse, filtered) without changing the collection's interface.

Native Iterator vs GoF

The GoF structure (with hasNext()/next() in separate classes) is useful for learning the pattern and for languages without native iteration protocols. In modern TypeScript, the Symbol.iterator protocol with generators (function*) is more idiomatic — it automatically manages cursor state via yield, without needing a separate ConcreteIterator class. In PHP, IteratorAggregate + Generator follows the same principle.

Structure

       «interface»
      ListIterator<T>
  ┌──────────────────────────────┐
  │ + hasNext(): boolean         │
  │ + next(): T                  │
  └──────────────────────────────┘
              ▲
  LinkedListIterator<T>
  ┌──────────────────────────────┐
  │ - current: Node<T> | null    │
  │ + hasNext(): boolean         │
  │ + next(): T                  │
  └──────────────────────────────┘
              ▲ creates
              │
       «interface»
       Aggregate<T>
  ┌──────────────────────────────────────┐
  │ + createIterator(): ListIterator<T>  │
  └──────────────────────────────────────┘
              ▲
  LinkedList<T> (ConcreteAggregate)
  ┌──────────────────────────────────────┐
  │ - head: Node<T> | null               │
  │ + add(value: T): void                │
  │ + createIterator(): ListIterator<T>  │
  └──────────────────────────────────────┘


Usage flow (GoF style):

  const it = list.createIterator();
  while (it.hasNext()) {
    console.log(it.next());   // no access to head, Node or next
  }

Native flow (TypeScript):

  for (const item of list) { ... }  // uses Symbol.iterator implicitly

Code examples

Example 1 — Linked list with a manual Iterator (GoF style)

A classic implementation of the pattern: ListIterator<T> as the interface, LinkedListIterator<T> as the ConcreteIterator, and LinkedList<T> as the Aggregate. The client walks the collection with no access whatsoever to the internal node structure.

// ── Iterator interface (GoF style) ───────────────────────────
interface ListIterator<T> {
  hasNext(): boolean;
  next(): T;
}

// ── Internal list node ───────────────────────────────────────
// Package-private class — the client never sees it.
class Node<T> {
  next: Node<T> | null = null;
  constructor(readonly value: T) {}
}

// ── ConcreteIterator ──────────────────────────────────────────
class LinkedListIterator<T> implements ListIterator<T> {
  private current: Node<T> | null;

  constructor(start: Node<T> | null) {
    this.current = start;
  }

  hasNext(): boolean {
    return this.current !== null;
  }

  next(): T {
    if (this.current === null) {
      throw new Error('Iterator: no more elements');
    }
    const value = this.current.value;
    this.current = this.current.next;
    return value;
  }
}

// ── ConcreteAggregate ─────────────────────────────────────────
class LinkedList<T> {
  private head: Node<T> | null = null;
  private tail: Node<T> | null = null;

  add(value: T): void {
    const node = new Node(value);
    if (this.tail === null) {
      this.head = this.tail = node;
    } else {
      this.tail.next = node;
      this.tail = node;
    }
  }

  // Each call returns an iterator with its own cursor.
  createIterator(): ListIterator<T> {
    return new LinkedListIterator(this.head);
  }
}

// ── Usage ────────────────────────────────────────────────────
const list = new LinkedList<string>();
list.add('Apple');
list.add('Banana');
list.add('Cherry');

const it = list.createIterator();
while (it.hasNext()) {
  console.log(it.next());
}
// Apple
// Banana
// Cherry

// Two simultaneous iterators over the same list — independent cursors:
const it1 = list.createIterator();
const it2 = list.createIterator();
console.log(it1.next()); // Apple
console.log(it2.next()); // Apple  (independent cursor)
console.log(it1.next()); // Banana

Example 2 — Native protocol: Symbol.iterator / Generator (TS) and IteratorAggregate / Generator (PHP)

Modern languages bake Iterator in as a native protocol. In TypeScript, implementing [Symbol.iterator]() as a generator (function* / a method with yield) makes the collection compatible with for...of, spread, and destructuring — without needing a separate ConcreteIterator class. In PHP, IteratorAggregate + Generator achieves the same result with far less code than the 5 methods of the Iterator interface.

// Linked list with the native Symbol.iterator protocol via a generator.
// No ConcreteIterator classes — yield manages the cursor automatically.

class Node<T> {
  next: Node<T> | null = null;
  constructor(readonly value: T) {}
}

class LinkedList<T> {
  private head: Node<T> | null = null;
  private tail: Node<T> | null = null;

  add(value: T): void {
    const node = new Node(value);
    if (!this.tail) {
      this.head = this.tail = node;
    } else {
      this.tail.next = node;
      this.tail = node;
    }
  }

  // Generator implementing Symbol.iterator — makes the list natively iterable.
  *[Symbol.iterator](): Generator<T> {
    let current = this.head;
    while (current !== null) {
      yield current.value;   // suspends here and hands the value to the caller
      current = current.next;
    }
  }

  // Second generator: a filtered iterator (multiple traversal strategies).
  *filter(predicate: (item: T) => boolean): Generator<T> {
    for (const item of this) {   // reuses Symbol.iterator
      if (predicate(item)) yield item;
    }
  }
}

// ── Usage ────────────────────────────────────────────────────
const list = new LinkedList<number>();
[3, 1, 4, 1, 5, 9, 2, 6].forEach(n => list.add(n));

// for...of uses Symbol.iterator implicitly.
for (const n of list) {
  process.stdout.write(n + ' ');
}
// 3 1 4 1 5 9 2 6

console.log();

// Spread also uses Symbol.iterator.
const arr = [...list];
console.log(arr);   // [3, 1, 4, 1, 5, 9, 2, 6]

// Filtered iterator: odd values only.
for (const n of list.filter(n => n % 2 !== 0)) {
  process.stdout.write(n + ' ');
}
// 3 1 1 5 9

console.log();

// Multiple independent simultaneous traversals are safe:
// each call to the generator creates a new object with its own cursor.
const itA = list[Symbol.iterator]();
const itB = list[Symbol.iterator]();
console.log(itA.next().value);   // 3
console.log(itB.next().value);   // 3  (independent cursor)
console.log(itA.next().value);   // 1

When to use

  • When you have a custom collection and don't want to expose its internal structure (nodes, cursors, indexes) to whoever traverses it. Iterator provides sequential access without leaking implementation details.
  • When you need multiple traversal forms over the same collection — forward, reverse, breadth-first, depth-first, filtered. Each strategy becomes a separate Iterator returned by the collection.
  • To make custom collections compatible with native loops: in TypeScript, Symbol.iterator enables for...of, spread, and destructuring; in PHP, IteratorAggregate enables foreach — without any change to the collection's public API.
  • When the traversal code needs to be reused in multiple places without duplication — the Iterator encapsulates the loop in a single definition.

When to avoid

  • For arrays and simple collections that are already natively iterable: the pattern adds complexity with no benefit when the collection is already an array or a type the language's loop already supports natively.
  • When the collection will always be traversed in a single way: if there's no variation in traversal strategy and the structure is stable, a toArray() method that returns an array is simpler and sufficient.

Pros and cons

Pros

  • Decouples the traversal algorithm from the data structure — switching from a linked list to a tree doesn't break the client.
  • Supports multiple simultaneous, independent iterators over the same collection, each with its own cursor.
  • Multiple traversal strategies (forward, reverse, filtered) without modifying the collection's interface.
  • Native integration with the language's loops via Symbol.iterator (TS) and Iterator/IteratorAggregate (PHP).
  • Generators eliminate the separate ConcreteIterator class, drastically reducing the required code.

Cons

  • Overhead of creating an iterator object for simple collections where an index would suffice.
  • Modifying the collection during iteration can invalidate the iterator's cursor — undefined behavior that requires care.
  • Class-based iterators (GoF style) require more code than the functional approach with generators.

Common pitfalls

1. Invalidating the iterator by modifying the collection during iteration

The most dangerous pitfall: adding or removing elements from the collection while an iterator traverses it can skip elements, visit the same element twice, or cause null-access exceptions. In Java this raises ConcurrentModificationException; in TypeScript and PHP the behavior is undefined and silent — the worst kind of bug.

Golden rule: never modify the collection during iteration with the same iterator. If you need to filter and remove elements, collect the indexes or items to remove into a separate list and modify the collection after the loop has finished.

2. Leaking the internal structure through the Iterator

An Iterator that exposes internal nodes (Node<T>) instead of values (T) defeats the pattern's purpose. The client ends up accessing it.next().value and then .next directly — encapsulation has been broken. The Iterator should expose only the data type the client needs, never the internal containers.

3. State shared between iterators

If a collection stores the internal cursor as an instance field (instead of returning a separate iterator object with its own cursor), two concurrent loops "steal" positions from each other. The fix: every call to createIterator() or getIterator() should return a new object with its own cursor. Generators in TypeScript and PHP solve this automatically — each generator invocation creates an independent instance.

4. Forgetting rewind in PHP

When using PHP's Iterator interface (not IteratorAggregate), foreach calls rewind() before starting the loop. If rewind() doesn't properly reset the cursor, a second foreach over the same instance starts from where the first one stopped — surprising behavior. With IteratorAggregate + Generator this doesn't happen: each foreach calls getIterator() and gets a fresh Generator with a zeroed cursor.

Related patterns

Iterator integrates with structural patterns that organize hierarchical collections:

Composite organizes objects into tree structures (part-whole); Iterator is the natural complement for traversing it. An Iterator can implement breadth-first (BFS) or depth-first (DFS) traversal over the Composite tree — the client uses the same iteration interface regardless of the traversal strategy chosen. Visitor is Iterator's counterpart for operations over structures: while Iterator traverses sequentially and hands each element to the caller, Visitor traverses the structure and applies a specific operation to each node type — separating the processing algorithm from the Composite's structure.