RizTech Academy logo
RizTech Academy
Data Structures, TypedLesson 3 of 330 min

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* with for 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

  1. Write a function taking Iterable<T> and confirm it accepts an array, a string, and a Set.
  2. Write function* countTo(n): Generator<number> and loop over it and spread it into an array.
  3. Write an infinite naturals() generator and take three values with .next(). Confirm it does not run forever.
  4. Make a NumberRange class iterable with *[Symbol.iterator]() and use it in for...of and spread.
  5. Confirm the yielded values are typed (the loop variable has the right type).
  6. (Stretch) Write an async function* that yields values with a delay, and consume it with for await...of.
  7. Take a case you were about to solve with a generator and decide whether a plain array would be simpler. Justify.

Official documentation

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