TypeScript Iterators
An iterator is any object with a next() method that hands back one value at a time and
tells you when it has run out. That is the entire idea. Arrays, strings, Map, Set and
every generator you have ever written are built on it, which is why for...of and the
spread operator work on all of them and on nothing else.
TypeScript adds three interfaces on top — Iterator, Iterable and IterableIterator —
and most of the confusion around this topic comes from not knowing which one to reach for.
This page covers the protocol, those three types, how to write your own, and the two
compiler settings that decide whether any of it actually compiles.
The iterator protocol
Every built-in collection exposes an iterator under a well-known symbol. Pull it out by hand and you can see the machinery:
const colourIterator = ['red', 'green'][Symbol.iterator]()
colourIterator.next() // { value: 'red', done: false }
colourIterator.next() // { value: 'green', done: false }
colourIterator.next() // { value: undefined, done: true }Each call returns an IteratorResult — an object with a value and a done flag. It is
a discriminated union, keyed on done, and
one of the few places the standard library uses that pattern on your behalf:
[object Object]Narrowing on done is what makes manual consumption type-safe. Check the flag and
TypeScript knows value is a real T rather than T | undefined:
function drain<T>(source: Iterator<T>): T[] {
const collected: T[] = []
let step = source.next()
while (!step.done) {
collected.push(step.value)
step = source.next()
}
return collected
}
const drained = drain(new Set([1, 2, 3]).values())
// number[]You will almost never write that loop. It is worth seeing once, because it is exactly what
for...of compiles down to.
Iterator, Iterable and IterableIterator
These three names get used interchangeably in blog posts and they are not the same thing.
An iterator is the thing with next(). An iterable is anything that can produce
an iterator, via a [Symbol.iterator]() method. Stripped of their generic parameters, the
two interfaces are this small:
interface MiniIterator<T> {
next(): { value: T; done: boolean }
}
interface MiniIterable<T> {
[Symbol.iterator](): MiniIterator<T>
}IterableIterator<T> is both at once: it has next(), and its [Symbol.iterator]()
returns itself. That self-reference is the useful part — it means you can for...of the
same object you are calling next() on.
The separation exists because an iterable is a factory and an iterator is a position. An
array is iterable and can be walked a hundred times, because each for...of asks for a
fresh iterator. A generator object is an iterator that happens to also be iterable, so it
has exactly one position and is exhausted after a single pass. That asymmetry explains a
class of bug that otherwise looks like nonsense: spread a generator into two arrays and
the second one comes back empty, while doing the same with an array works fine. Both are
Iterable<T> as far as the type system is concerned — the types say nothing about how
many times you get to read.
The practical rule: accept Iterable<T>, return IterableIterator<T>. Accepting an
iterable makes your function work on arrays, sets, maps, strings and generators without
any overloads:
function takeFirst<T>(source: Iterable<T>): T | undefined {
for (const item of source) {
return item
}
return undefined
}
takeFirst('typescript') // string | undefined
takeFirst(new Map([['a', 1]])) // [string, number] | undefinedNote the second result. Map iterates as key/value pairs, and TypeScript types those as a
tuple rather than a loose array.
Consuming an iterable
Three syntaxes read from the protocol, and they all work on the same set of types:
const scoreBoard = new Map([
['ada', 12],
['grace', 19],
])
for (const [player, score] of scoreBoard) {
console.log(`${player}: ${score}`)
}
const allPlayers = [...scoreBoard.keys()]
// string[]
const [topEntry] = scoreBoard
// [string, number]The spread operator is the fastest way to
materialise an iterable into an array, and it is the usual move when you want array
methods on something that is not an array. Array destructuring reads from the iterator
too, which is why you can destructure a Set or a generator, not just an array.
Plain objects are not iterable
This trips up almost everyone once. Object literals do not implement the protocol, so
for...of rejects them outright:
const featureFlags = { darkMode: true, betaBanner: false }
// for (const flag of featureFlags) {}
// ❌ Type '{ darkMode: boolean; betaBanner: boolean; }' is not an array type
// or a string type
for (const [flagName, enabled] of Object.entries(featureFlags)) {
console.log(flagName, enabled)
}for...in does work on objects, but it walks keys — including inherited ones — and it
has no relationship to the iterator protocol at all. The two loops share three letters and
nothing else. Reach for Object.entries when you want an
object's contents as something iterable; it hands you an array of key/value tuples, and
everything on this page applies from there.
Writing your own iterator
Implementing Iterable<T> on a class is the classic payoff.
Written out longhand, you return an object with a next() that closes over some state:
class NumberRange implements Iterable<number> {
constructor(
private readonly from: number,
private readonly to: number,
) {}
[Symbol.iterator](): Iterator<number> {
let cursor = this.from
const ceiling = this.to
return {
next(): IteratorResult<number> {
if (cursor > ceiling) {
return { value: undefined, done: true }
}
return { value: cursor++, done: false }
},
}
}
}
const rangeTotal = [...new NumberRange(1, 4)].reduce((sum, n) => sum + n, 0)
// 10Because NumberRange satisfies the protocol, it works with for...of and spread with no
further effort. The implements Iterable<number> clause is optional but worth keeping —
it turns a typo in the method name into a compile error instead of a value that silently
refuses to iterate.
Generators write the iterator for you
Hand-rolling next() gets old quickly. A generator function does the same job and keeps
the state in ordinary local variables:
function* fibonacci(limitCount: number): Generator<number> {
let previous = 0
let upcoming = 1
for (let emitted = 0; emitted < limitCount; emitted++) {
yield previous
;[previous, upcoming] = [upcoming, previous + upcoming]
}
}
const firstEight = [...fibonacci(8)]
// [0, 1, 1, 2, 3, 5, 8, 13]Generator<T> extends IterableIterator<T>, so everything above applies. You can also
use a generator as the [Symbol.iterator] method directly, which collapses the
NumberRange example to three lines:
class LetterBag implements Iterable<string> {
constructor(private readonly word: string) {}
*[Symbol.iterator]() {
for (const letter of this.word) {
yield letter.toUpperCase()
}
}
}
const shouted = [...new LetterBag('ts')]
// ['T', 'S']Laziness is the real reason to use them
Nothing in a generator runs until something pulls on it. That makes an infinite sequence perfectly safe to write, as long as the consumer stops asking:
function* naturals(): Generator<number> {
let tally = 1
while (true) {
yield tally++
}
}
function takeCount<T>(source: Iterable<T>, count: number): T[] {
const picked: T[] = []
for (const value of source) {
if (picked.length >= count) break
picked.push(value)
}
return picked
}
const firstFive = takeCount(naturals(), 5)
// [1, 2, 3, 4, 5]naturals() never finishes, and that is fine — takeCount calls next() exactly five
times and then walks away. Do the same thing with an array and you have to decide up front
how many numbers you need. This is the whole argument for pipelines built on iterables
rather than arrays: no intermediate collections, and no work done for results nobody reads.
Breaking out of a for...of is not just a jump, either. It calls the iterator's optional
return() method, which is how a generator gets told to clean up:
function* readLines(source: readonly string[]): Generator<string> {
try {
for (const entry of source) {
yield entry
}
} finally {
console.log('reader closed')
}
}
for (const line of readLines(['a', 'b', 'c'])) {
if (line === 'b') break
}
// logs 'reader closed'The finally block runs on the break, not at the end of the array. If your generator
holds a file handle or a database cursor, that is where you release it — and it is the
reason to prefer for...of over a hand-written while (!step.done) loop, which has no
such guarantee.
Pick IterableIterator for return types
This is the mistake that costs people an afternoon. Annotate a function that returns a
generator as Iterator<T> and you throw away the ability to iterate the result:
function evenNumbers(source: Iterable<number>): IterableIterator<number> {
function* filterEven() {
for (const value of source) {
if (value % 2 === 0) {
yield value
}
}
}
return filterEven()
}
const evens = [...evenNumbers([1, 2, 3, 4])]
// [2, 4]Swap the return type to Iterator<number> and the call site breaks, even though the
runtime value is unchanged:
function oddNumbers(source: Iterable<number>): Iterator<number> {
function* filterOdd() {
for (const value of source) {
if (value % 2 === 1) {
yield value
}
}
}
return filterOdd()
}
// const odds = [...oddNumbers([1, 2, 3])]
// ❌ Type 'Iterator<number>' must have a '[Symbol.iterator]()' method that
// returns an iteratorThe type is narrower than the value. Iterator<T> promises only next(), so spread and
for...of have nothing to call. Use Iterator<T> for a parameter you are going to pump
manually, and IterableIterator<T> — or Generator<T> — everywhere else.
The compiler settings that decide all of this
Two tsconfig.json fields control whether iteration compiles at all.
lib supplies the type declarations. Iterating a Map or Set needs es2015.iterable
in scope; without it the collections exist but have no [Symbol.iterator], and the error
you get is about for...of rather than about lib, which sends people in the wrong
direction.
target decides the emitted JavaScript. At es2015 or above the protocol ships natively
and for...of stays for...of. At es5 the compiler rewrites it into an index loop,
which is faster but only correct for arrays and strings — and not even fully correct for
strings:
const emojiSpread = [...'a🙂b']
// ['a', '🙂', 'b'] — the string iterator walks code points
const emojiSplit = 'a🙂b'.split('')
// ['a', '\ud83d', '\ude42', 'b'] — the index loop walks code unitsdownlevelIteration is the fix. It tells the compiler to emit the real protocol when
targeting ES5, at the cost of a helper function per loop. If you are targeting ES5 and
iterating anything other than a plain array, turn it on.
Two things make this worth knowing even on a modern target. The first is that the bug is
silent: 'a🙂b' has a length of 4, so the ES5 index loop produces four items and every
one of them type-checks as a string. Nothing fails until a user with an emoji in their
display name files a ticket. The second is that most projects inherit target from a
framework preset rather than choosing it, so the setting that governs this is usually one
nobody on the team has read. If you are shipping to browsers that still need ES5, the
honest answer is to set downlevelIteration once and stop thinking about it — the helper
it emits is a few dozen bytes, and the class of bug it removes is the kind that survives
code review.
Wrap up
An iterator is an object with next(). An iterable is an object that can hand you one. A
Symbol.iterator method is all it takes to join in, and a generator writes that method
for you.
The rule worth remembering is the asymmetry in signatures: take Iterable<T> so callers
can pass anything, hand back IterableIterator<T> so they can keep iterating what you
returned. Generic helpers built this way compose with the rest of the standard library for
free — see generics if the type parameters above went past quickly,
and union types for the narrowing that makes IteratorResult
safe to read.
If you want the mechanics to stick, the Zip challenge asks you to walk two sequences in step at the type level, which is the same problem this protocol solves at runtime.
Become a TypeScript Pro
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges