|$ 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
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 |
| 2 | interface IteratorYieldResult<TYield> { |
| 3 | done?: false; |
| 4 | value: TYield; |
| 5 | } |
| 6 | |
| 7 | interface IteratorReturnResult<TReturn> { |
| 8 | done: true; |
| 9 | value: TReturn; |
| 10 | } |
| 11 | |
| 12 | type IteratorResult<TYield, TReturn = undefined> = |
| 13 | | IteratorYieldResult<TYield> |
| 14 | | IteratorReturnResult<TReturn>; |
| 15 | |
| 16 | // Manual iterator |
| 17 | const 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 |
| 30 | let result = numbers.next(); |
| 31 | while (!result.done) { |
| 32 | console.log(result.value); // 1, 2, 3, 4, 5 |
| 33 | result = numbers.next(); |
| 34 | } |
| 35 | |
| 36 | // TypeScript infers the yield type |
| 37 | function 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 | |
| 49 | const counter = createCounter(1, 3); |
| 50 | counter.next(); // { done: false, value: 1 } |
| 51 | counter.next(); // { done: false, value: 2 } |
| 52 | counter.next(); // { done: false, value: 3 } |
| 53 | counter.next(); // { done: true, value: undefined } |
Iterable Protocol
iterable.ts
TypeScript
| 1 | // An iterable implements [Symbol.iterator]() returning an Iterator |
| 2 | interface Iterable<T> { |
| 3 | [Symbol.iterator](): Iterator<T>; |
| 4 | } |
| 5 | |
| 6 | // Range — a custom iterable that yields numbers in a range |
| 7 | class 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 |
| 36 | const range = new Range(1, 10, 2); |
| 37 | for (const n of range) { |
| 38 | console.log(n); // 1, 3, 5, 7, 9 |
| 39 | } |
| 40 | |
| 41 | // Spread into array |
| 42 | const arr = [...new Range(0, 5)]; // [0, 1, 2, 3, 4] |
| 43 | |
| 44 | // Destructure |
| 45 | const [first, second, ...rest] = new Range(1, 10); |
| 46 | console.log(first, second, rest); // 1, 2, [3,4,5,6,7,8,9] |
| 47 | |
| 48 | // Array.from from iterable |
| 49 | const doubled = Array.from(new Range(1, 5), (n) => n * 2); |
| 50 | // [2, 4, 6, 8, 10] |
| 51 | |
| 52 | // String is also iterable |
| 53 | for (const char of "hello") { |
| 54 | console.log(char); // "h", "e", "l", "l", "o" |
| 55 | } |
| 56 | |
| 57 | // Map and Set are iterables |
| 58 | const map = new Map([["a", 1], ["b", 2]]); |
| 59 | for (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 |
| 5 | function* countTo(n: number): Generator<number> { |
| 6 | for (let i = 1; i <= n; i++) { |
| 7 | yield i; |
| 8 | } |
| 9 | } |
| 10 | |
| 11 | for (const n of countTo(5)) { |
| 12 | console.log(n); // 1, 2, 3, 4, 5 |
| 13 | } |
| 14 | |
| 15 | // Generator with return value |
| 16 | function* 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 |
| 26 | function* 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 | |
| 35 | const fib20 = [...take(fibonacci(), 20)]; |
| 36 | console.log(fib20); // First 20 Fibonacci numbers |
| 37 | |
| 38 | // Generator with yield* delegation |
| 39 | function* flatten<T>(nested: Iterable<Iterable<T>>): Generator<T> { |
| 40 | for (const inner of nested) { |
| 41 | yield* inner; |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | const flattened = [...flatten([[1, 2], [3, 4], [5]])]; |
| 46 | // [1, 2, 3, 4, 5] |
| 47 | |
| 48 | // Generator with next() input — two-way communication |
| 49 | function* 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 | |
| 55 | const d = dialogue(); |
| 56 | console.log(d.next()); // { value: "What is your name?", done: false } |
| 57 | console.log(d.next("Alice")); // { value: "Hello, Alice! What do you want?", done: false } |
| 58 | console.log(d.next("help")); // { value: "OK, help", done: true } |
| 59 | |
| 60 | // Typed generator with done return |
| 61 | function* 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) |
| 2 | const arr = ["a", "b", "c"]; |
| 3 | for (const val of arr) { |
| 4 | console.log(val); // "a", "b", "c" |
| 5 | } |
| 6 | |
| 7 | // for...in — iterates over KEYS (enumerable properties) |
| 8 | const obj = { x: 1, y: 2, z: 3 }; |
| 9 | for (const key in obj) { |
| 10 | console.log(key, obj[key as keyof typeof obj]); |
| 11 | } |
| 12 | |
| 13 | // Common mistake: for...in on an array |
| 14 | const nums = [10, 20, 30]; |
| 15 | for (const x in nums) { |
| 16 | console.log(x); // "0", "1", "2" — strings, not numbers! |
| 17 | } |
| 18 | for (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 |
| 26 | for (const [key, value] of Object.entries(obj)) { |
| 27 | console.log(`${key}: ${value}`); |
| 28 | } |
| 29 | |
| 30 | // for...of with Map — yields [key, value] pairs |
| 31 | const map = new Map([["a", 1], ["b", 2]]); |
| 32 | for (const [key, value] of map) { |
| 33 | console.log(key, value); |
| 34 | } |
| 35 | |
| 36 | // for...of with Set — yields values |
| 37 | const set = new Set([1, 2, 3]); |
| 38 | for (const val of set) { |
| 39 | console.log(val); |
| 40 | } |
| 41 | |
| 42 | // Break and continue work with for...of |
| 43 | for (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. |
| 2 | interface AsyncIterable<T> { |
| 3 | [Symbol.asyncIterator](): AsyncIterator<T>; |
| 4 | } |
| 5 | |
| 6 | interface AsyncIterator<T> { |
| 7 | next(): Promise<IteratorResult<T>>; |
| 8 | } |
| 9 | |
| 10 | // Async generator — async function with yield |
| 11 | async 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 |
| 27 | async 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 |
| 39 | async 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 |
| 48 | async 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 |
| 58 | async 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 |
| 66 | async 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 |
| 2 | class 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 |
| 47 | const 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 | |
| 54 | const adminNames = users |
| 55 | .filter((u) => u.role === "admin") |
| 56 | .map((u) => u.name) |
| 57 | .toArray(); |
| 58 | // ["Alice", "Charlie"] |
| 59 | |
| 60 | // Works with for...of |
| 61 | for (const name of users.filter((u) => u.age > 27).map((u) => u.name)) { |
| 62 | console.log(name); |
| 63 | } |
| 64 | |
| 65 | // Tree traversal as iterable |
| 66 | class 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 | |
| 89 | const tree = new TreeNode(1); |
| 90 | tree.children.push(new TreeNode(2), new TreeNode(3)); |
| 91 | tree.children[0].children.push(new TreeNode(4), new TreeNode(5)); |
| 92 | |
| 93 | const values = [...tree]; // [1, 2, 4, 5, 3] — depth-first |
$Blueprint — Engineering Documentation·Section ID: TS-ITER·Revision: 1.0