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:
-
Iterator (interface): declares the traversal
operations. GoF style:
hasNext(): booleanandnext(): T. In PHP's SPL:current(),next(),rewind(),valid(),key(). In TypeScript: the method[Symbol.iterator]()returning an object withnext(): IteratorResult<T>. -
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. -
Aggregate (interface): declares the iterator
creation method. E.g.:
createIterator(): ListIterator<T>. - 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
<?php
// PHP has a native Iterator interface (SPL) with 5 methods.
// Implementing it makes the class usable directly in a foreach.
// ── Internal node ────────────────────────────────────────────
class Node
{
public ?Node $next = null;
public function __construct(public readonly mixed $value) {}
}
// ── ConcreteAggregate implementing Iterator (SPL) ─────────────
// Here the list and the iterator are the same class — a compact approach.
// For independent simultaneous cursors, split into a LinkedListIterator class.
class LinkedList implements Iterator
{
private ?Node $head = null;
private ?Node $tail = null;
private ?Node $cursor = null;
private int $position = 0;
public function add(mixed $value): void
{
$node = new Node($value);
if ($this->tail === null) {
$this->head = $this->tail = $node;
} else {
$this->tail->next = $node;
$this->tail = $node;
}
}
// ── Iterator interface methods (SPL) ─────────────────────
public function rewind(): void
{
$this->cursor = $this->head;
$this->position = 0;
}
public function valid(): bool
{
return $this->cursor !== null;
}
public function current(): mixed
{
return $this->cursor?->value;
}
public function key(): int
{
return $this->position;
}
public function next(): void
{
$this->cursor = $this->cursor?->next;
$this->position++;
}
}
// ── Usage ────────────────────────────────────────────────────
$list = new LinkedList();
$list->add('Apple');
$list->add('Banana');
$list->add('Cherry');
// Native foreach, thanks to the Iterator (SPL) interface.
foreach ($list as $index => $fruit) {
echo "{$index}: {$fruit}\n";
}
// 0: Apple
// 1: Banana
// 2: Cherry
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
<?php
// IteratorAggregate + Generator: a simpler implementation than the Iterator interface.
// getIterator() returns a Generator — PHP automatically treats it as Traversable.
class Node
{
public ?Node $next = null;
public function __construct(public readonly mixed $value) {}
}
class LinkedList implements IteratorAggregate
{
private ?Node $head = null;
private ?Node $tail = null;
public function add(mixed $value): void
{
$node = new Node($value);
if ($this->tail === null) {
$this->head = $this->tail = $node;
} else {
$this->tail->next = $node;
$this->tail = $node;
}
}
// Generator that replaces the 5 methods of Iterator with a single yield.
// Each foreach creates a new Generator instance with its own cursor.
public function getIterator(): Generator
{
$current = $this->head;
while ($current !== null) {
yield $current->value; // suspends; hands the value to the foreach
$current = $current->next;
}
}
// Filtered generator: a second traversal strategy.
public function filter(callable $predicate): Generator
{
foreach ($this as $item) { // reuses getIterator()
if ($predicate($item)) {
yield $item;
}
}
}
}
// ── Usage ────────────────────────────────────────────────────
$list = new LinkedList();
foreach ([3, 1, 4, 1, 5, 9, 2, 6] as $n) {
$list->add($n);
}
// Native foreach via IteratorAggregate.
foreach ($list as $n) {
echo $n . ' ';
}
// 3 1 4 1 5 9 2 6
echo PHP_EOL;
// Conversion to array.
$arr = iterator_to_array($list, false);
print_r($arr); // [3, 1, 4, 1, 5, 9, 2, 6]
// Filtered iterator: odd values only.
foreach ($list->filter(fn($n) => $n % 2 !== 0) as $n) {
echo $n . ' ';
}
// 3 1 1 5 9
echo PHP_EOL;
// Independent simultaneous traversals:
// each foreach calls getIterator() and gets a fresh Generator.
$genA = $list->getIterator();
$genB = $list->getIterator();
$genA->current(); // 3
$genB->current(); // 3 (independent cursor)
$genA->next();
echo $genA->current() . "\n"; // 1
echo $genB->current() . "\n"; // 3 (genB still on the first)
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.iteratorenablesfor...of, spread, and destructuring; in PHP,IteratorAggregateenablesforeach— 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.