Coding · Design patterns
Iterator pattern explained with examples
Short answer
Iterator is a behavioral design pattern that provides a way to access the elements of a collection one after another without exposing how the collection is stored. The traversal logic lives in a separate iterator object with a method such as next(), so one collection can offer several traversal orders and client code can loop over arrays, trees and streams the same way. In TypeScript and JavaScript the pattern is built into the language through Symbol.iterator and for...of.
Intent and the problem it solves
Intent: provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation. Iterator is one of the behavioral patterns in the Gang of Four (GoF) book.
Imagine a class that stores a company's org chart as a tree. Different parts of the program need to list all employees: one report goes level by level, another needs depth-first order. If each report walks the tree itself, every report depends on the tree's internal fields, and changing the tree to use a different storage breaks all of them.
With Iterator, the tree offers iterator objects. A report asks for an iterator and calls next() until it is done. The report does not know whether the data is a tree, an array or rows fetched from a database page by page.
Structure and participants
| Role | Responsibility |
|---|---|
| Iterator | Interface for traversal, typically next() returning the next element and a flag that tells whether the traversal is finished |
| Concrete iterator | Implements traversal for one collection and remembers the current position |
| Aggregate (iterable) | Interface with a method that creates an iterator |
| Concrete aggregate | The collection; returns a new concrete iterator each time one is requested |
| Client | Uses iterators to visit elements without knowing the collection's structure |
In JavaScript these roles have standard names, defined by the iteration protocols in ES2015. An iterable has a [Symbol.iterator]() method. An iterator has a next() method that returns { value, done }. Every for...of loop, spread [...x] and destructuring assignment uses these protocols.
TypeScript implementation: a hand-written iterator
This range class stores only its start, end and step, not a list of numbers. Its iterator computes each value on demand.
class Range implements Iterable<number> {
constructor(private start: number, private end: number, private step = 1) {}
[Symbol.iterator](): Iterator<number> {
let current = this.start;
const { end, step } = this;
return {
next(): IteratorResult<number> {
if (current >= end) return { value: undefined, done: true };
const value = current;
current += step;
return { value, done: false };
},
};
}
}
const evens = new Range(0, 10, 2);
for (const n of evens) console.log(n); // 0 2 4 6 8 (one per line)
console.log([...evens]); // [ 0, 2, 4, 6, 8 ]
console.log(Math.max(...new Range(1, 4))); // 3Each call to [Symbol.iterator]() returns a fresh iterator with its own current position. That is why evens can be looped over twice. If the collection returned the same iterator every time, the second loop would see no elements.
Generators: iterators with less code
A generator function, written with function*, creates an iterator automatically. Each yield produces the next value, and the function pauses until next() is called again. Here a binary tree offers an in-order traversal without exposing its nodes:
class TreeNode<T> {
constructor(public value: T, public left?: TreeNode<T>, public right?: TreeNode<T>) {}
}
class BinaryTree<T> implements Iterable<T> {
constructor(private root?: TreeNode<T>) {}
*[Symbol.iterator](): Generator<T> {
yield* inOrder(this.root);
}
}
function* inOrder<T>(node?: TreeNode<T>): Generator<T> {
if (!node) return;
yield* inOrder(node.left);
yield node.value;
yield* inOrder(node.right);
}
const tree = new BinaryTree(
new TreeNode(5, new TreeNode(3, new TreeNode(1)), new TreeNode(8)),
);
console.log([...tree]); // [ 1, 3, 5, 8 ]
for (const value of tree) {
if (value > 3) break; // stops early; the rest is never computed
console.log(value); // 1, then 3
}When to use it and when not to
- Use it when a collection has a complex internal structure (tree, graph, paged API) and you want clients to loop over it simply.
- Use it to offer several traversal orders over the same data, such as depth-first and breadth-first.
- Use it for lazy or infinite sequences, where values are computed only when requested.
- Use it to give different collections one uniform loop interface.
- Do not write a custom iterator for data that is already an array. Return the array (or a read-only copy) and let callers use the built-in iteration.
- Do not use a lazy iterator when callers need random access by index or the length up front; an array is a better fit.
Real-world uses
- JavaScript:
Array,Map,SetandStringare iterables, andmap.entries()orset.values()return iterators. - Java: the
java.util.Iteratorinterface withhasNext()andnext(), used by the enhancedforloop on anyIterable. - Python: the iterator protocol with
__iter__and__next__, and generator functions usingyield. - Databases and APIs: cursors over query results and paginated HTTP APIs expose results one page at a time, so clients can iterate over very large results without loading everything into memory.
Related patterns
| Pattern | How it relates to Iterator |
|---|---|
| Composite | Iterators are often used to traverse Composite trees without exposing their nodes. |
| Visitor | Iterator walks the elements; Visitor defines what to do with each element. They are often used together. |
| Factory Method | The method that creates an iterator, such as [Symbol.iterator](), is a factory method: each collection returns its own iterator class. |
| Memento | An iterator can save its position as a memento so that traversal can be resumed later. |
FAQ
What is the difference between an iterable and an iterator?
An iterable is an object that can produce an iterator, through [Symbol.iterator]() in JavaScript. An iterator is the object that keeps the current position and returns the elements one at a time through next().
Is a generator an iterator?
Yes. Calling a generator function returns a generator object, which is both an iterator (it has next()) and an iterable (it returns itself from [Symbol.iterator]()).
Why not just expose the internal array?
Exposing the array ties every caller to that storage choice and lets them modify it. An iterator hides the structure, so you can change it later or compute elements lazily without changing callers.
What is an internal iterator?
An internal iterator is a method that takes a callback and runs it for every element, such as array.forEach(fn). The collection controls the loop. An external iterator, like next(), lets the client control when to move forward and when to stop.