|$ curl https://forge-ai.dev/api/markdown?path=docs/typescript/iteration
$cat docs/typescript-—-iteration-protocols.md
updated Last week·15 min read·published

TypeScript — Iteration Protocols

TypeScriptIntermediate🎯Free Tools
Introduction

JavaScript defines two iteration protocols: the iterable protocol and the iterator protocol. These protocols enable custom collections to work with for...of, spread syntax, destructuring, and Array.from. TypeScript adds type-level guarantees on top, ensuring iterables produce the correct value types.

Iterator Protocol

An iterator is an object with a next() method that returns { value, done }. The iterator protocol requires this shape — nothing else.

iterator.ts
TypeScript
1// Iterator interface in TypeScript
2interface IteratorYieldResult<TYield> {
3 done?: false;
4 value: TYield;
5}
6
7interface IteratorReturnResult<TReturn> {
8 done: true;
9 value: TReturn;
10}
11
12type IteratorResult<TYield, TReturn = undefined> =
13 | IteratorYieldResult<TYield>
14 | IteratorReturnResult<TReturn>;
15
16// Manual iterator
17const numbers: Iterator<number> = {
18 current: 1,
19 last: 5,
20
21 next(): IteratorResult<number> {
22 if (this.current <= this.last) {
23 return { done: false, value: this.current++ };
24 }
25 return { done: true, value: undefined };
26 },
27};
28
29// Consuming the iterator manually
30let result = numbers.next();
31while (!result.done) {
32 console.log(result.value); // 1, 2, 3, 4, 5
33 result = numbers.next();
34}
35
36// TypeScript infers the yield type
37function createCounter(start: number, end: number): Iterator<number> {
38 let current = start;
39 return {
40 next() {
41 if (current <= end) {
42 return { done: false, value: current++ };
43 }
44 return { done: true, value: undefined };
45 },
46 };
47}
48
49const counter = createCounter(1, 3);
50counter.next(); // { done: false, value: 1 }
51counter.next(); // { done: false, value: 2 }
52counter.next(); // { done: false, value: 3 }
53counter.next(); // { done: true, value: undefined }
Iterable Protocol
iterable.ts
TypeScript
1// An iterable implements [Symbol.iterator]() returning an Iterator
2interface Iterable<T> {
3 [Symbol.iterator](): Iterator<T>;
4}
5
6// Range — a custom iterable that yields numbers in a range
7class Range implements Iterable<number> {
8 constructor(
9 private start: number,
10 private end: number,
11 private step: number = 1
12 ) {}
13
14 [Symbol.iterator](): Iterator<number> {
15 let current = this.start;
16 const end = this.end;
17 const step = this.step;
18
19 return {
20 next(): IteratorResult<number> {
21 if (step > 0 ? current < end : current > end) {
22 return { done: false, value: current };
23 }
24 // Do not increment past the end — just return current then done
25 if (current === end || (step > 0 && current + step > end) || (step < 0 && current + step < end)) {
26 return { done: false, value: current };
27 }
28 current += step;
29 return { done: false, value: current };
30 },
31 };
32 }
33}
34
35// Usage
36const range = new Range(1, 10, 2);
37for (const n of range) {
38 console.log(n); // 1, 3, 5, 7, 9
39}
40
41// Spread into array
42const arr = [...new Range(0, 5)]; // [0, 1, 2, 3, 4]
43
44// Destructure
45const [first, second, ...rest] = new Range(1, 10);
46console.log(first, second, rest); // 1, 2, [3,4,5,6,7,8,9]
47
48// Array.from from iterable
49const doubled = Array.from(new Range(1, 5), (n) => n * 2);
50// [2, 4, 6, 8, 10]
51
52// String is also iterable
53for (const char of "hello") {
54 console.log(char); // "h", "e", "l", "l", "o"
55}
56
57// Map and Set are iterables
58const map = new Map([["a", 1], ["b", 2]]);
59for (const [key, value] of map) {
60 console.log(key, value);
61}

info

Any object with a [Symbol.iterator] method is iterable. This includes arrays, strings, Maps, Sets, NodeList, arguments objects, and custom classes.
Generators
generators.ts
TypeScript
1// Generator functions implement both Iterable and Iterator
2// They are the easiest way to create custom iterables
3
4// Basic generator — yields values lazily
5function* countTo(n: number): Generator<number> {
6 for (let i = 1; i <= n; i++) {
7 yield i;
8 }
9}
10
11for (const n of countTo(5)) {
12 console.log(n); // 1, 2, 3, 4, 5
13}
14
15// Generator with return value
16function* fibonacci(): Generator<number> {
17 let a = 0;
18 let b = 1;
19 while (true) {
20 yield a;
21 [a, b] = [b, a + b];
22 }
23}
24
25// Take first N from infinite generator
26function* take<T>(iterable: Iterable<T>, count: number): Generator<T> {
27 let taken = 0;
28 for (const item of iterable) {
29 if (taken >= count) return;
30 yield item;
31 taken++;
32 }
33}
34
35const fib20 = [...take(fibonacci(), 20)];
36console.log(fib20); // First 20 Fibonacci numbers
37
38// Generator with yield* delegation
39function* flatten<T>(nested: Iterable<Iterable<T>>): Generator<T> {
40 for (const inner of nested) {
41 yield* inner;
42 }
43}
44
45const flattened = [...flatten([[1, 2], [3, 4], [5]])];
46// [1, 2, 3, 4, 5]
47
48// Generator with next() input — two-way communication
49function* dialogue(): Generator<string, string, string> {
50 const greeting = yield "What is your name?";
51 const action = yield `Hello, ${greeting}! What do you want?`;
52 return `OK, ${action}`;
53}
54
55const d = dialogue();
56console.log(d.next()); // { value: "What is your name?", done: false }
57console.log(d.next("Alice")); // { value: "Hello, Alice! What do you want?", done: false }
58console.log(d.next("help")); // { value: "OK, help", done: true }
59
60// Typed generator with done return
61function* parseJson<T>(lines: string[]): Generator<string, T[], void> {
62 const results: string[] = [];
63 for (const line of lines) {
64 if (line.startsWith("#")) continue; // skip comments
65 results.push(line);
66 yield line;
67 }
68 return results as unknown as T[];
69}
for...of vs for...in
for_of.ts
TypeScript
1// for...of — iterates over VALUES (iterable protocol)
2const arr = ["a", "b", "c"];
3for (const val of arr) {
4 console.log(val); // "a", "b", "c"
5}
6
7// for...in — iterates over KEYS (enumerable properties)
8const obj = { x: 1, y: 2, z: 3 };
9for (const key in obj) {
10 console.log(key, obj[key as keyof typeof obj]);
11}
12
13// Common mistake: for...in on an array
14const nums = [10, 20, 30];
15for (const x in nums) {
16 console.log(x); // "0", "1", "2" — strings, not numbers!
17}
18for (const x of nums) {
19 console.log(x); // 10, 20, 30 — actual values
20}
21
22// for...of does NOT work on plain objects
23// for (const v of {}) {} // TypeError: {} is not iterable
24
25// Solution: Object.entries, Object.keys, Object.values
26for (const [key, value] of Object.entries(obj)) {
27 console.log(`${key}: ${value}`);
28}
29
30// for...of with Map — yields [key, value] pairs
31const map = new Map([["a", 1], ["b", 2]]);
32for (const [key, value] of map) {
33 console.log(key, value);
34}
35
36// for...of with Set — yields values
37const set = new Set([1, 2, 3]);
38for (const val of set) {
39 console.log(val);
40}
41
42// Break and continue work with for...of
43for (const n of fibonacci()) {
44 if (n > 100) break;
45 if (n % 2 !== 0) continue;
46 console.log(n); // Even Fibonacci numbers under 100
47}
Async Iteration
async_iteration.ts
TypeScript
1// Async iterable — for streaming data, pagination, etc.
2interface AsyncIterable<T> {
3 [Symbol.asyncIterator](): AsyncIterator<T>;
4}
5
6interface AsyncIterator<T> {
7 next(): Promise<IteratorResult<T>>;
8}
9
10// Async generator — async function with yield
11async function* fetchPages(url: string): AsyncGenerator<Response> {
12 let page = 1;
13 let hasMore = true;
14
15 while (hasMore) {
16 const response = await fetch(`${url}?page=${page}`);
17 const data = await response.json();
18
19 yield response;
20
21 hasMore = data.nextPage !== null;
22 page++;
23 }
24}
25
26// Consuming with for await...of
27async function getAllUsers() {
28 const allUsers: any[] = [];
29
30 for await (const response of fetchPages("/api/users")) {
31 const data = await response.json();
32 allUsers.push(...data.users);
33 }
34
35 return allUsers;
36}
37
38// Async iterable from array of promises
39async function* promiseAll<T>(
40 promises: Promise<T>[]
41): AsyncGenerator<T> {
42 for (const p of promises) {
43 yield await p;
44 }
45}
46
47// Using async generator for event streams
48async function* watchFile(path: string): AsyncGenerator<string> {
49 const watcher = fs.watch(path);
50 for await (const event of watcher) {
51 if (event.eventType === "change") {
52 yield fs.readFileSync(path, "utf-8");
53 }
54 }
55}
56
57// for await...of — works on any async iterable
58async function processStream() {
59 const stream = getDataStream();
60 for await (const chunk of stream) {
61 process.stdout.write(chunk);
62 }
63}
64
65// Async generator with return value
66async function* paginate<T>(
67 fetcher: (page: number) => Promise<T[]>
68): AsyncGenerator<T[], T[]> {
69 const allResults: T[] = [];
70 let page = 0;
71
72 while (true) {
73 const results = await fetcher(page);
74 if (results.length === 0) break;
75 allResults.push(...results);
76 page++;
77 }
78
79 return allResults;
80}

info

Use for await...of to consume async iterables. Each iteration waits for the previous promise to resolve before proceeding — this is ideal for paginated APIs, WebSocket messages, and file streams.
Custom Iterable Collection
custom_iterables.ts
TypeScript
1// A type-safe iterable collection with filtering and mapping
2class Query<T> implements Iterable<T> {
3 private items: T[];
4
5 constructor(items: T[]) {
6 this.items = items;
7 }
8
9 [Symbol.iterator](): Iterator<T> {
10 let index = 0;
11 const items = this.items;
12 return {
13 next(): IteratorResult<T> {
14 if (index < items.length) {
15 return { done: false, value: items[index++] };
16 }
17 return { done: true, value: undefined };
18 },
19 };
20 }
21
22 filter(predicate: (item: T) => boolean): Query<T> {
23 return new Query(this.items.filter(predicate));
24 }
25
26 map<R>(transform: (item: T) => R): Query<R> {
27 return new Query(this.items.map(transform));
28 }
29
30 toArray(): T[] {
31 return [...this];
32 }
33
34 first(): T | undefined {
35 for (const item of this) return item;
36 return undefined;
37 }
38
39 count(): number {
40 let n = 0;
41 for (const _ of this) n++;
42 return n;
43 }
44}
45
46// Usage — chainable iterable pipeline
47const users = new Query([
48 { name: "Alice", age: 30, role: "admin" },
49 { name: "Bob", age: 25, role: "user" },
50 { name: "Charlie", age: 35, role: "admin" },
51 { name: "Diana", age: 28, role: "user" },
52]);
53
54const adminNames = users
55 .filter((u) => u.role === "admin")
56 .map((u) => u.name)
57 .toArray();
58// ["Alice", "Charlie"]
59
60// Works with for...of
61for (const name of users.filter((u) => u.age > 27).map((u) => u.name)) {
62 console.log(name);
63}
64
65// Tree traversal as iterable
66class TreeNode<T> implements Iterable<T> {
67 children: TreeNode<T>[] = [];
68
69 constructor(public value: T) {}
70
71 [Symbol.iterator](): Iterator<T> {
72 const stack: TreeNode<T>[] = [this];
73 return {
74 next(): IteratorResult<T> {
75 while (stack.length > 0) {
76 const node = stack.pop()!;
77 // Push children in reverse for left-to-right traversal
78 for (let i = node.children.length - 1; i >= 0; i--) {
79 stack.push(node.children[i]);
80 }
81 return { done: false, value: node.value };
82 }
83 return { done: true, value: undefined };
84 },
85 };
86 }
87}
88
89const tree = new TreeNode(1);
90tree.children.push(new TreeNode(2), new TreeNode(3));
91tree.children[0].children.push(new TreeNode(4), new TreeNode(5));
92
93const values = [...tree]; // [1, 2, 4, 5, 3] — depth-first
$Blueprint — Engineering Documentation·Section ID: TS-ITER·Revision: 1.0