BehavioralTypeScriptverifiedVerified
Iterator Pattern in TypeScript
Provides a way to sequentially access elements of a collection without exposing its underlying representation.
How to Implement the Iterator Pattern in TypeScript
1Step 1: Define the Iterator and Iterable interfaces
interface Iterator<T> {
hasNext(): boolean;
next(): T;
}
interface Iterable<T> {
createIterator(): Iterator<T>;
}2Step 2: Implement the collection and its iterator
class NumberRange implements Iterable<number> {
constructor(
private readonly start: number,
private readonly end: number,
private readonly step: number = 1
) {}
createIterator(): Iterator<number> {
return new RangeIterator(this.start, this.end, this.step);
}
}
class RangeIterator implements Iterator<number> {
private current: number;
constructor(
private start: number,
private end: number,
private step: number
) {
this.current = start;
}
hasNext(): boolean {
return this.current <= this.end;
}
next(): number {
if (!this.hasNext()) throw new Error("No more elements");
const value = this.current;
this.current += this.step;
return value;
}
}3Step 3: Traverse the range using the iterator
const range = new NumberRange(1, 10, 2);
const iter = range.createIterator();
while (iter.hasNext()) {
console.log(iter.next()); // 1, 3, 5, 7, 9
}// ── Paginated API Iterator (Async) ────────────────────────────────
interface Page<T> {
items: T[];
nextCursor: string | null;
totalCount: number;
}
interface FetchPage<T> {
(cursor: string | null, pageSize: number): Promise<Page<T>>;
}
class PaginatedIterator<T> implements AsyncIterator<T> {
private buffer: T[] = [];
private cursor: string | null = null;
private done = false;
private fetched = 0;
constructor(
private readonly fetchPage: FetchPage<T>,
private readonly pageSize: number = 20,
private readonly maxItems?: number
) {}
async next(): Promise<IteratorResult<T>> {
// Enforce maxItems limit
if (this.maxItems !== undefined && this.fetched >= this.maxItems) {
return { value: undefined as unknown as T, done: true };
}
// Refill buffer if empty
if (this.buffer.length === 0) {
if (this.done) return { value: undefined as unknown as T, done: true };
const page = await this.fetchPage(this.cursor, this.pageSize);
this.buffer = page.items;
this.cursor = page.nextCursor;
if (page.nextCursor === null) this.done = true;
if (this.buffer.length === 0) return { value: undefined as unknown as T, done: true };
}
this.fetched++;
return { value: this.buffer.shift()!, done: false };
}
[Symbol.asyncIterator](): AsyncIterator<T> {
return this;
}
}
// ── Utility: collect all items ────────────────────────────────────
async function collectAll<T>(
fetchPage: FetchPage<T>,
options: { pageSize?: number; maxItems?: number } = {}
): Promise<T[]> {
const results: T[] = [];
const iterator = new PaginatedIterator(fetchPage, options.pageSize, options.maxItems);
for await (const item of iterator) {
results.push(item);
}
return results;
}
// ── Usage ─────────────────────────────────────────────────────────
interface GitHubRepo { id: number; name: string; stargazers_count: number }
async function fetchGitHubRepos(
org: string,
cursor: string | null,
pageSize: number
): Promise<Page<GitHubRepo>> {
const page = cursor ? parseInt(cursor, 10) : 1;
const url = `https://api.github.com/orgs/${org}/repos?per_page=${pageSize}&page=${page}`;
const res = await fetch(url);
if (!res.ok) throw new Error(`GitHub API error: ${res.status}`);
const items: GitHubRepo[] = await res.json();
return {
items,
nextCursor: items.length === pageSize ? String(page + 1) : null,
totalCount: -1,
};
}
export { PaginatedIterator, collectAll, type Page, type FetchPage };Iterator Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Iterator Pattern in the Real World
“Think of a TV remote control. Whether your playlist is on a Blu-ray disc, a streaming service, or a USB drive, you use the same next and previous buttons to cycle through content. The remote (iterator interface) abstracts away the completely different internal mechanisms each media source uses to retrieve the next item.”