Iterator
Advanced · 10 min read · ▶ live playground · ✦ checkpoint
A team roster exposes its members array so the dashboard can loop over names. The roster later moves to linked nodes for cheaper inserts, and every client that indexed the old array breaks.
The Iterator pattern provides sequential access to a collection without exposing how that collection stores its elements. It preserves the collection boundary, but a custom iterator adds cursor state, end-of-sequence rules, and another object to test.
Before: clients walk the storage structure
The dashboard does not ask the roster for members. It reaches into the roster's representation and controls the traversal itself:
class TeamRoster {
public members: string[] = [];
}
const roster = new TeamRoster();
roster.members.push("Ada", "Lin", "Mo");
for (let index = 0; index < roster.members.length; index += 1) {
console.log(roster.members[index]);
}The public array is now part of the roster's contract. Clients can reorder it, delete entries without checking an invariant, and depend on numeric indexes. Replacing it with a tree, linked list, database page, or generated sequence is no longer an internal change.
Returning a copied array protects mutation but still forces the collection to allocate and reveal one eager, array-shaped view. A lazy or paginated collection may not have all its elements yet.
After: give traversal its own object
An iterator is an object that remembers a traversal position and returns elements one at a time. hasNext() answers whether another element exists; next() advances the cursor and returns that element.
An iterable is an object that can create an iterator. Creating a fresh iterator per traversal matters: two clients can walk the same collection independently without sharing a cursor.
class diagram
TeamRoster creates an iterator over hidden linked nodes
- 1Iterator defines the cursor operations without revealing an array, tree, or linked node.
- 2Iterable promises to create an Iterator, giving each traversal its own cursor.
- 3RosterIterator implements the cursor contract by following one private linked-node chain.
- 4TeamRoster is iterable, so clients request traversal instead of reading its storage.
- 5The collection creates and returns a RosterIterator positioned at its current head.
TeamRoster owns the nodes, while RosterIterator owns one traversal cursor. The collection can change the node implementation without changing the two operations clients use.
The code calls the teaching interfaces ClassicIterator and ClassicIterable so their names do not collide with TypeScript's built-in iterator types:
interface ClassicIterator<T> {
hasNext(): boolean;
next(): T;
}
interface ClassicIterable<T> {
iterator(): ClassicIterator<T>;
}
interface MemberNode {
value: string;
next: MemberNode | null;
}
class RosterIterator implements ClassicIterator<string> {
public constructor(private current: MemberNode | null) {}
public hasNext(): boolean {
return this.current !== null;
}
public next(): string {
if (this.current === null) {
throw new Error("Iterator exhausted");
}
const value = this.current.value;
this.current = this.current.next;
return value;
}
}
class TeamRoster implements ClassicIterable<string>, Iterable<string> {
private head: MemberNode | null = null;
private tail: MemberNode | null = null;
public add(name: string): void {
const node: MemberNode = { value: name, next: null };
if (this.tail === null) {
this.head = node;
this.tail = node;
return;
}
this.tail.next = node;
this.tail = node;
}
public iterator(): ClassicIterator<string> {
return new RosterIterator(this.head);
}
public *[Symbol.iterator](): IterableIterator<string> {
let current = this.head;
while (current !== null) {
yield current.value;
current = current.next;
}
}
}
const roster = new TeamRoster();
roster.add("Ada");
roster.add("Lin");
const cursor = roster.iterator();
console.log(`first: ${cursor.next()}`);
console.log("for...of:");
for (const member of roster) {
console.log(member);
}first: Ada for...of: Ada Lin
The manual cursor advances once and returns "Ada". The later for...of starts from the head because [Symbol.iterator]() creates a separate traversal; it does not reuse the manual cursor.
JavaScript and TypeScript already standardize the native protocol. An object is usable with for...of when its [Symbol.iterator]() method returns a native iterator whose next() reports { value, done }. A generator method is a *-marked method that can pause at yield; here, it builds the native iterator while preserving the collection's private representation.
What the iterator can hide
The client sees a sequence even when the source is not a flat in-memory list:
- A tree iterator can keep a stack and choose depth-first or breadth-first order.
- A graph iterator can remember visited nodes and avoid cycles.
- A lazy iterator can calculate the next value only when requested.
- A paginated iterator can fetch another page when its current page is exhausted.
That uniform surface does not erase source behavior. A remote page can fail, and an infinite sequence never becomes exhausted. Iterator hides representation, not cost or failure modes; name and document those semantics at the collection boundary.
When NOT to use it
For a plain array, use its built-in iterator, for...of, map, or another standard operation. Wrapping an array in ArrayIterator adds code without creating a useful abstraction.
A hand-rolled iterator earns its place when traversal itself has state or policy: trees, graphs, lazy calculations, filtered streams, or paginated results. Even then, prefer the native [Symbol.iterator]() protocol when consumers only need synchronous iteration. It integrates with the language and already defines exhaustion behavior.
Custom hasNext() and next() contracts also need decisions about mutation. What happens if the roster changes halfway through a walk: does the iterator see the new member, use a snapshot, or fail fast? Iterator trades exposed storage for a traversal lifecycle that must be specified and tested.
Checkpoint
Answer all three to mark this lesson complete
Iterator separates visiting elements from storing them. Next, Mediator separates a group of colleagues from the web of direct references they would otherwise need.