Skip to article
ALGORITHMICSPatterns
Patterns6 min read

Iterator

Walking a collection without knowing its shape — and why laziness is the part that matters.


You have a list of numbers and you want the total.

for (let i = 0; i < values.length; i++) sum += values[i];

That loop knows three things about the container: it has a length, it is indexed from zero, and indexing is cheap. Swap the array for a linked list and all three are wrong — values[i] walks from the head, and the loop silently becomes O(n2)O(n^2).

The pattern

Ask the collection for an object that knows how to walk it.

interface Iterator<T> {
next(): {value: T; done: false} | {value: undefined; done: true};
}

The loop then depends only on that, and works for anything that can produce one:

Container
const values = [3, 1, 4, 1, 5];

for (const v of values) sum += v;   // identical in all three
31415

The simplest case, and the reason the interface exists: the loop never mentions indices, so it survives the container being replaced by either of the other two.

Three containers with nothing structurally in common — a contiguous array, a tree with no linear order, and a sequence that is not stored at all — and the loop over them is character-for-character identical.

Laziness is the real payoff

next() produces one element on demand. It does not need the rest to exist.

function* naturals() {
let n = 1;
while (true) yield n++; // infinite, and perfectly safe
}
function* take<T>(source: Iterable<T>, count: number) {
for (const value of source) {
if (count-- <= 0) return;
yield value;
}
}
[...take(naturals(), 5)]; // [1, 2, 3, 4, 5]

Nothing here allocates a list of naturals. That is the property that makes iterators useful beyond tidiness: a pipeline over ten million rows costs one row of memory, because each element is pulled through the whole chain before the next is requested.

The eager version — rows.map(...).filter(...).slice(0, 10) — builds two full intermediate arrays to return ten items.

In this language, write a generator

Implementing next() by hand means storing the traversal position yourself, and for a tree that means an explicit stack:

class TreeIterator implements Iterator<number> {
private stack: Node[] = [];
// …twenty lines of manual state management…
}

A generator has the position stored for you, in the function’s own suspended execution:

function* inorder(node: Node | undefined): Generator<number> {
if (!node) return;
yield* inorder(node.left);
yield node.value;
yield* inorder(node.right);
}

Four lines, and it reads exactly like the recursive traversal it is. Implement [Symbol.iterator] with one and your class works with for…of, spread, destructuring and Array.from for free.

External and internal

The version above is external: the caller drives, calling next() and deciding when to stop. That is what allows break, early return, and consuming two iterators in lockstep.

The other kind is internal: you hand the collection a function and it drives.

values.forEach((v) => { sum += v; }); // internal

Internal iteration is simpler to implement and easier to parallelise — nothing requires the elements to be visited in order — which is why parallel collection APIs are always internal. It gives up early exit, which is why forEach cannot break.