Typing iterables, iterators and generators
Behind every for...of loop and every spread of a collection is a shared protocol: iterables and
iterators. And generators are a special kind of function that produces a sequence lazily, one
value at a time. These are less-everyday than arrays and maps, but they are how you process streams,
build lazy sequences, and type custom collections — and understanding them completes your grasp of
TypeScript's data structures. This lesson types all three.
The iterable protocol — what for...of walks
Anything you can loop over with for...of is an iterable — an array, a string, a Map, a Set.
TypeScript types this with the Iterable<T> interface:
function printAll<T>(items: Iterable<T>): void {
for (const item of items) {
console.log(item); // item is T
}
}
printAll([1, 2, 3]); // arrays are Iterable<number>
printAll("Pune"); // strings are Iterable<string> — iterates characters
printAll(new Set(["a", "b"])); // Sets are Iterable<string>
Typing a parameter as Iterable<T> instead of T[] says "I will loop over this, and I do not care
whether it is an array, a set, or anything else iterable" — more flexible than requiring an array. This is
the right type for a function that only iterates. The related Iterator<T> is the lower-level object with
a next() method that produces { value, done } — you rarely write one by hand, because generators
create them for you.
Generators — producing a sequence lazily
A generator is a function that can pause and resume, yielding values one at a time. It is written
function* (with an asterisk) and uses yield:
function* countTo(n: number): Generator<number> {
for (let i = 1; i <= n; i++) {
yield i; // produce a value and pause here until the next value is requested
}
}
for (const num of countTo(3)) {
console.log(num); // 1, 2, 3 — num is number
}
const nums = [...countTo(3)]; // [1, 2, 3] — spread a generator into an array
countTo is a generator returning Generator<number> — an iterable you can for...of or spread. Each
yield i produces a value and pauses the function; the next value is computed only when requested. This
laziness is the point: values are produced on demand, not all at once. TypeScript types the yielded
values (Generator<number> means it yields numbers), so num in the loop is a number.
The killer feature of laziness: a generator can be infinite, because you only take what you need:
function* naturals(): Generator<number> {
let n = 1;
while (true) { // infinite loop — but that is fine
yield n++;
}
}
const gen = naturals();
gen.next().value; // 1
gen.next().value; // 2
gen.next().value; // 3 — produced on demand; the generator never "runs forever"
naturals() yields numbers forever, but because it is lazy, calling next() produces just one more each
time — you never compute an infinite list. This is impossible with an array (you cannot build an infinite
array) and is exactly what generators are for: an open-ended or expensive sequence where you take a few
values. take(gen, 5) patterns and lazy pipelines build on this.
Typing a generator's yield, return, and next
Generator<T> can be more precise — it has three type parameters for the three things a generator does:
function* example(): Generator<number, string, boolean> {
// ^yield ^return ^next-argument
const answer = yield 1; // answer is boolean (the value passed to next())
yield 2;
return "done"; // the return value is string
}
Generator<Yield, Return, Next> types what it yields (number), what it returns when done
(string), and what type of value can be passed into next() (boolean). In practice you almost
always use just the first (Generator<number>, the yield type), because the return and next-argument
features are advanced. But knowing the full shape explains the type when you see it.
Making a class iterable
You can make your own class iterable — usable in for...of and spread — by implementing the iterator
protocol, most easily with a generator method named [Symbol.iterator]:
class NumberRange {
constructor(private start: number, private end: number) {}
*[Symbol.iterator](): Generator<number> {
for (let i = this.start; i <= this.end; i++) {
yield i;
}
}
}
const range = new NumberRange(1, 5);
for (const n of range) console.log(n); // 1, 2, 3, 4, 5
const arr = [...range]; // [1, 2, 3, 4, 5]
Implementing *[Symbol.iterator]() as a generator makes NumberRange iterable — for...of and spread
now work on it, and TypeScript types the yielded values. This is how you build a custom collection that
behaves like a built-in one, and it is the cleanest way to expose iteration from your own data structure
(a linked list, a tree traversal, a paginated result). The generator does the hard work of the iterator
protocol for you.
When you actually use these
Be honest about frequency: you use iterables constantly (every for...of), but you write generators
and custom iterators occasionally. The genuine everyday uses:
- Lazy or infinite sequences — producing values on demand, taking only what you need (an id generator, a paginated fetch, a lazy transform pipeline).
- Custom collections — making your own data structure iterable with
[Symbol.iterator]. - Processing streams — async generators (
async function*withfor await...of) type a stream of values that arrive over time, combining generators with the async module — the shape behind reading a large file or a paginated API chunk by chunk.
For ordinary "loop over a list and transform it", map/filter/for...of on an array are simpler and
right — do not reach for a generator where an array does the job (over-engineering again). But when you
need laziness (infinite, expensive, or on-demand) or a custom iterable, generators are the tool, and
now you can type them. Recognising them in code — the function*, the yield, the Symbol.iterator — is
the main goal; writing them is for the specific cases that need it.
Check your work
What an iterable is. Anything you can for...of — arrays, strings, Map, Set — typed
Iterable<T>; use it for a parameter you only loop over.
What a generator is. A function* that yields values one at a time, pausing at each yield —
producing a lazy sequence.
Why laziness matters. Values are produced on demand, so a generator can be infinite — you take only what you need, which an array cannot do.
The three Generator type parameters. Yield type, return type, and next-argument type — you usually
use only the yield type.
How to make a class iterable. Implement *[Symbol.iterator](): Generator<T> — then for...of and
spread work on it.
The everyday uses. Lazy/infinite sequences, custom collections, and stream processing (async generators).
When not to use a generator. For ordinary list processing — map/filter/for...of on an array is
simpler; reach for generators only when you need laziness or a custom iterable.
Practice
- Write a function taking
Iterable<T>and confirm it accepts an array, a string, and aSet. - Write
function* countTo(n): Generator<number>and loop over it and spread it into an array. - Write an infinite
naturals()generator and take three values with.next(). Confirm it does not run forever. - Make a
NumberRangeclass iterable with*[Symbol.iterator]()and use it infor...ofand spread. - Confirm the yielded values are typed (the loop variable has the right type).
- (Stretch) Write an
async function*that yields values with a delay, and consume it withfor await...of. - Take a case you were about to solve with a generator and decide whether a plain array would be simpler. Justify.
Official documentation
- TypeScript — Iterators and Generators — The full feature.
- MDN — Iteration protocols — Iterable and iterator, from the source.
- MDN — Generators —
function*,yield, and laziness.
Next module — Classes, Modules and Decorators: the object-oriented side, including the decorators NestJS is built on.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship