DevDocsDev Docs
Design PatternsBehavioral Patterns

Iterator

Provide a way to access elements of a collection sequentially without exposing its underlying representation

Iterator Pattern

Intent

Iterator is a behavioral design pattern that lets you traverse elements of a collection without exposing its underlying representation (list, stack, tree, etc.).


Problem It Solves

Different collections have different traversal needs:

  • Arrays: sequential access
  • Trees: depth-first, breadth-first
  • Graphs: various traversal orders
  • Custom structures: domain-specific iteration

Without Iterator:

  • Client code is coupled to collection structure
  • Can't change collection without breaking clients
  • Traversal logic scattered everywhere

Solution

Extract traversal logic into separate iterator objects:


TypeScript Iterators

TypeScript/JavaScript has built-in iterator support:

/**
 * Custom iterable using Symbol.iterator
 * @description Makes any object usable in for...of loops
 */
interface Range {
  start: number;
  end: number;
  step: number;
  [Symbol.iterator]: () => Iterator<number>;
}

const createRange = (start: number, end: number, step = 1): Range => ({
  start,
  end,
  step,
  [Symbol.iterator]() {
    let current = start;
    return {
      next() {
        if ((step > 0 && current < end) || (step < 0 && current > end)) {
          const value = current;
          current += step;
          return { value, done: false };
        }
        return { value: undefined, done: true };
      },
    };
  },
});

// Usage with for...of
const range = createRange(1, 5);
for (const num of range) {
  console.log(num);
}
// Outputs: 1, 2, 3, 4

// Works with spread operator
const arr = [...createRange(0, 10, 2)];
console.log(arr); // [0, 2, 4, 6, 8]

Implementation

/**
 * Generator-based iterators
 * @description Using TypeScript generators for clean iteration
 */

/**
 * Infinite sequence generator
 */
function* infiniteSequence(start = 0): Generator<number> {
  let n = start;
  while (true) {
    yield n++;
  }
}

/**
 * Fibonacci sequence generator
 */
function* fibonacci(): Generator<number> {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

/**
 * Take first n elements from any iterator
 */
function* take<T>(iterator: Iterator<T>, count: number): Generator<T> {
  let taken = 0;
  while (taken < count) {
    const { value, done } = iterator.next();
    if (done) break;
    yield value;
    taken++;
  }
}

/**
 * Filter iterator
 */
function* filter<T>(
  iterator: Iterator<T>,
  predicate: (item: T) => boolean
): Generator<T> {
  while (true) {
    const { value, done } = iterator.next();
    if (done) break;
    if (predicate(value)) {
      yield value;
    }
  }
}

/**
 * Map iterator
 */
function* map<T, U>(
  iterator: Iterator<T>,
  transform: (item: T) => U
): Generator<U> {
  while (true) {
    const { value, done } = iterator.next();
    if (done) break;
    yield transform(value);
  }
}

/**
 * Zip two iterators
 */
function* zip<T, U>(
  iter1: Iterator<T>,
  iter2: Iterator<U>
): Generator<[T, U]> {
  while (true) {
    const result1 = iter1.next();
    const result2 = iter2.next();
    if (result1.done || result2.done) break;
    yield [result1.value, result2.value];
  }
}

/**
 * Flatten nested iterables
 */
function* flatten<T>(
  iterable: Iterable<Iterable<T>>
): Generator<T> {
  for (const inner of iterable) {
    yield* inner;
  }
}

// Usage
console.log("First 10 Fibonacci numbers:");
const fibs = [...take(fibonacci(), 10)];
console.log(fibs);
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

console.log("\nEven Fibonacci numbers under 100:");
const evenFibs = [
  ...take(
    filter(fibonacci(), n => n % 2 === 0 && n < 100),
    10
  )
];
console.log(evenFibs);
// [0, 2, 8, 34]

console.log("\nSquares of first 5 numbers:");
const squares = [...take(map(infiniteSequence(1), n => n * n), 5)];
console.log(squares);
// [1, 4, 9, 16, 25]

console.log("\nZipped:");
const zipped = [...zip(
  infiniteSequence(1)[Symbol.iterator]?.() ?? infiniteSequence(1),
  ["a", "b", "c"][Symbol.iterator]()
)];
console.log(zipped);
// [[1, "a"], [2, "b"], [3, "c"]]
/**
 * Tree node structure
 */
interface TreeNode<T> {
  value: T;
  children: TreeNode<T>[];
}

/**
 * Create a tree node
 */
const createNode = <T>(value: T, children: TreeNode<T>[] = []): TreeNode<T> => ({
  value,
  children,
});

/**
 * Tree with multiple traversal iterators
 */
interface Tree<T> {
  root: TreeNode<T> | null;
  /** Depth-first pre-order traversal */
  preOrder: () => Generator<T>;
  /** Depth-first post-order traversal */
  postOrder: () => Generator<T>;
  /** Breadth-first (level-order) traversal */
  levelOrder: () => Generator<T>;
  /** Iterate leaves only */
  leaves: () => Generator<T>;
}

const createTree = <T>(root: TreeNode<T> | null): Tree<T> => ({
  root,

  *preOrder() {
    if (!root) return;
    
    const stack: TreeNode<T>[] = [root];
    while (stack.length > 0) {
      const node = stack.pop()!;
      yield node.value;
      // Add children in reverse order for correct traversal
      for (let i = node.children.length - 1; i >= 0; i--) {
        stack.push(node.children[i]);
      }
    }
  },

  *postOrder() {
    function* traverse(node: TreeNode<T>): Generator<T> {
      for (const child of node.children) {
        yield* traverse(child);
      }
      yield node.value;
    }
    
    if (root) yield* traverse(root);
  },

  *levelOrder() {
    if (!root) return;
    
    const queue: TreeNode<T>[] = [root];
    while (queue.length > 0) {
      const node = queue.shift()!;
      yield node.value;
      queue.push(...node.children);
    }
  },

  *leaves() {
    function* findLeaves(node: TreeNode<T>): Generator<T> {
      if (node.children.length === 0) {
        yield node.value;
      } else {
        for (const child of node.children) {
          yield* findLeaves(child);
        }
      }
    }
    
    if (root) yield* findLeaves(root);
  },
});

// Build a tree:
//        1
//      / | \
//     2  3  4
//    /|     |
//   5 6     7

const root = createNode(1, [
  createNode(2, [
    createNode(5),
    createNode(6),
  ]),
  createNode(3),
  createNode(4, [
    createNode(7),
  ]),
]);

const tree = createTree(root);

console.log("Pre-order:", [...tree.preOrder()]);
// [1, 2, 5, 6, 3, 4, 7]

console.log("Post-order:", [...tree.postOrder()]);
// [5, 6, 2, 3, 7, 4, 1]

console.log("Level-order:", [...tree.levelOrder()]);
// [1, 2, 3, 4, 5, 6, 7]

console.log("Leaves:", [...tree.leaves()]);
// [5, 6, 3, 7]

// Use in for...of
console.log("\nIterating pre-order:");
for (const value of tree.preOrder()) {
  console.log(`  Node: ${value}`);
}
/**
 * Paginated API response
 */
interface PaginatedResponse<T> {
  data: T[];
  page: number;
  pageSize: number;
  totalPages: number;
  totalItems: number;
  hasNext: boolean;
}

/**
 * Paginated data source
 */
interface PaginatedSource<T> {
  /** Fetch a specific page */
  fetchPage: (page: number, pageSize: number) => Promise<PaginatedResponse<T>>;
  /** Create async iterator for all items */
  iterate: (pageSize?: number) => AsyncGenerator<T>;
  /** Create async iterator for pages */
  iteratePages: (pageSize?: number) => AsyncGenerator<PaginatedResponse<T>>;
}

/**
 * Mock user data
 */
interface User {
  id: number;
  name: string;
  email: string;
}

/**
 * Create paginated API source
 */
const createUserApi = (): PaginatedSource<User> => {
  // Simulated database
  const allUsers: User[] = Array.from({ length: 47 }, (_, i) => ({
    id: i + 1,
    name: `User ${i + 1}`,
    email: `user${i + 1}@example.com`,
  }));

  return {
    async fetchPage(page, pageSize) {
      // Simulate API latency
      await new Promise(resolve => setTimeout(resolve, 100));
      
      const start = (page - 1) * pageSize;
      const data = allUsers.slice(start, start + pageSize);
      const totalPages = Math.ceil(allUsers.length / pageSize);
      
      return {
        data,
        page,
        pageSize,
        totalPages,
        totalItems: allUsers.length,
        hasNext: page < totalPages,
      };
    },

    async *iterate(pageSize = 10) {
      let page = 1;
      let hasNext = true;

      while (hasNext) {
        const response = await this.fetchPage(page, pageSize);
        
        for (const item of response.data) {
          yield item;
        }
        
        hasNext = response.hasNext;
        page++;
      }
    },

    async *iteratePages(pageSize = 10) {
      let page = 1;
      let hasNext = true;

      while (hasNext) {
        const response = await this.fetchPage(page, pageSize);
        yield response;
        
        hasNext = response.hasNext;
        page++;
      }
    },
  };
};

/**
 * Async utility: Take first n items
 */
async function* takeAsync<T>(
  iter: AsyncGenerator<T>,
  count: number
): AsyncGenerator<T> {
  let taken = 0;
  for await (const item of iter) {
    if (taken >= count) break;
    yield item;
    taken++;
  }
}

/**
 * Async utility: Filter items
 */
async function* filterAsync<T>(
  iter: AsyncGenerator<T>,
  predicate: (item: T) => boolean
): AsyncGenerator<T> {
  for await (const item of iter) {
    if (predicate(item)) {
      yield item;
    }
  }
}

/**
 * Collect async iterator to array
 */
async function collect<T>(iter: AsyncGenerator<T>): Promise<T[]> {
  const items: T[] = [];
  for await (const item of iter) {
    items.push(item);
  }
  return items;
}

// Usage
const api = createUserApi();

// Iterate through all users
console.log("All users (first 5):");
for await (const user of takeAsync(api.iterate(), 5)) {
  console.log(`  ${user.id}: ${user.name}`);
}

// Filter users with even IDs
console.log("\nUsers with even IDs (first 3):");
const evenUsers = await collect(
  takeAsync(
    filterAsync(api.iterate(), user => user.id % 2 === 0),
    3
  )
);
console.log(evenUsers);

// Iterate through pages
console.log("\nIterating pages:");
for await (const page of api.iteratePages(15)) {
  console.log(`  Page ${page.page}/${page.totalPages}: ${page.data.length} users`);
}
/**
 * Query builder with lazy evaluation
 */
interface QueryIterator<T> {
  /** Filter items */
  where: (predicate: (item: T) => boolean) => QueryIterator<T>;
  /** Map items */
  select: <U>(transform: (item: T) => U) => QueryIterator<U>;
  /** Take first n items */
  take: (count: number) => QueryIterator<T>;
  /** Skip first n items */
  skip: (count: number) => QueryIterator<T>;
  /** Order by key */
  orderBy: <K extends keyof T>(key: K, direction?: "asc" | "desc") => QueryIterator<T>;
  /** Get distinct items */
  distinct: () => QueryIterator<T>;
  /** Group by key */
  groupBy: <K extends keyof T>(key: K) => QueryIterator<{ key: T[K]; items: T[] }>;
  /** Execute and collect results */
  toArray: () => T[];
  /** Execute and get first item */
  first: () => T | undefined;
  /** Execute and count items */
  count: () => number;
  /** Check if any match */
  any: (predicate?: (item: T) => boolean) => boolean;
  /** Check if all match */
  all: (predicate: (item: T) => boolean) => boolean;
  /** Iterate */
  [Symbol.iterator]: () => Iterator<T>;
}

const createQuery = <T>(source: Iterable<T>): QueryIterator<T> => {
  const query: QueryIterator<T> = {
    where(predicate) {
      function* filter() {
        for (const item of source) {
          if (predicate(item)) yield item;
        }
      }
      return createQuery(filter());
    },

    select<U>(transform: (item: T) => U) {
      function* map() {
        for (const item of source) {
          yield transform(item);
        }
      }
      return createQuery(map());
    },

    take(count) {
      function* take() {
        let taken = 0;
        for (const item of source) {
          if (taken >= count) break;
          yield item;
          taken++;
        }
      }
      return createQuery(take());
    },

    skip(count) {
      function* skip() {
        let skipped = 0;
        for (const item of source) {
          if (skipped < count) {
            skipped++;
            continue;
          }
          yield item;
        }
      }
      return createQuery(skip());
    },

    orderBy<K extends keyof T>(key: K, direction: "asc" | "desc" = "asc") {
      const items = [...source];
      items.sort((a, b) => {
        const aVal = a[key];
        const bVal = b[key];
        const cmp = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
        return direction === "asc" ? cmp : -cmp;
      });
      return createQuery(items);
    },

    distinct() {
      function* distinct() {
        const seen = new Set<string>();
        for (const item of source) {
          const key = JSON.stringify(item);
          if (!seen.has(key)) {
            seen.add(key);
            yield item;
          }
        }
      }
      return createQuery(distinct());
    },

    groupBy<K extends keyof T>(key: K) {
      const groups = new Map<T[K], T[]>();
      for (const item of source) {
        const groupKey = item[key];
        const group = groups.get(groupKey) || [];
        group.push(item);
        groups.set(groupKey, group);
      }
      
      return createQuery(
        Array.from(groups.entries()).map(([k, items]) => ({ key: k, items }))
      );
    },

    toArray() {
      return [...source];
    },

    first() {
      for (const item of source) {
        return item;
      }
      return undefined;
    },

    count() {
      let count = 0;
      for (const _ of source) {
        count++;
      }
      return count;
    },

    any(predicate) {
      for (const item of source) {
        if (!predicate || predicate(item)) return true;
      }
      return false;
    },

    all(predicate) {
      for (const item of source) {
        if (!predicate(item)) return false;
      }
      return true;
    },

    [Symbol.iterator]() {
      return source[Symbol.iterator]();
    },
  };

  return query;
};

// Usage
interface Product {
  id: number;
  name: string;
  category: string;
  price: number;
  inStock: boolean;
}

const products: Product[] = [
  { id: 1, name: "Laptop", category: "Electronics", price: 999, inStock: true },
  { id: 2, name: "Phone", category: "Electronics", price: 699, inStock: true },
  { id: 3, name: "Tablet", category: "Electronics", price: 499, inStock: false },
  { id: 4, name: "Desk", category: "Furniture", price: 299, inStock: true },
  { id: 5, name: "Chair", category: "Furniture", price: 199, inStock: true },
  { id: 6, name: "Book", category: "Books", price: 29, inStock: true },
  { id: 7, name: "Notebook", category: "Books", price: 15, inStock: false },
];

// Complex query: Electronics in stock, sorted by price, top 2
const topElectronics = createQuery(products)
  .where(p => p.category === "Electronics")
  .where(p => p.inStock)
  .orderBy("price", "desc")
  .take(2)
  .select(p => ({ name: p.name, price: p.price }))
  .toArray();

console.log("Top 2 in-stock electronics:", topElectronics);
// [{ name: "Laptop", price: 999 }, { name: "Phone", price: 699 }]

// Group by category
const byCategory = createQuery(products)
  .groupBy("category")
  .select(g => ({
    category: g.key,
    count: g.items.length,
    totalValue: g.items.reduce((sum, p) => sum + p.price, 0),
  }))
  .orderBy("totalValue", "desc")
  .toArray();

console.log("\nProducts by category:", byCategory);

// Check conditions
const hasExpensive = createQuery(products).any(p => p.price > 500);
const allInStock = createQuery(products).all(p => p.inStock);
console.log("\nHas expensive (>$500):", hasExpensive);
console.log("All in stock:", allInStock);

// Lazy iteration
console.log("\nLazy iteration (first 3 products under $300):");
for (const product of createQuery(products).where(p => p.price < 300).take(3)) {
  console.log(`  ${product.name}: $${product.price}`);
}

When to Use


Real-World Applications

TechnologyIterator Usage
JavaScriptfor...of, generators, Symbol.iterator
JavaIterator, Iterable interfaces
C#IEnumerable, LINQ
PythonIterators, generators
DatabasesCursors for result sets
Streaming APIsProcess data in chunks

Summary

Key Takeaway: Iterator provides uniform access to collection elements without exposing the underlying structure. TypeScript's generators make implementation elegant and memory-efficient.

Pros

  • ✅ Single Responsibility: Traversal logic separated from collection
  • ✅ Open/Closed: New iterators without changing collection
  • ✅ Parallel iteration: Multiple iterators on same collection
  • ✅ Lazy evaluation: Memory efficient for large collections

Cons

  • ❌ Overkill for simple collections
  • ❌ Less efficient than direct access for some algorithms
  • ❌ Iterators can become stale if collection changes

On this page