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 .
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:
const values = [3, 1, 4, 1, 5]; for (const v of values) sum += v; // identical in all three
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; }); // internalInternal 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.