+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 298 of 355

๐Ÿ“˜ Iterator Pattern: Collection Traversal

Master iterator pattern: collection traversal in TypeScript with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

Prerequisites

  • Basic understanding of JavaScript ๐Ÿ“
  • TypeScript installation โšก
  • VS Code or preferred IDE ๐Ÿ’ป

What you'll learn

  • Understand the concept fundamentals ๐ŸŽฏ
  • Apply the concept in real projects ๐Ÿ—๏ธ
  • Debug common issues ๐Ÿ›
  • Write type-safe code โœจ

๐ŸŽฏ Introduction

Welcome to this exciting tutorial on the Iterator Pattern! ๐ŸŽ‰ In this guide, weโ€™ll explore how to traverse collections in a clean, type-safe way using TypeScript.

Youโ€™ll discover how the Iterator Pattern can transform your approach to handling collections. Whether youโ€™re building playlists ๐ŸŽต, managing inventories ๐Ÿ“ฆ, or processing data streams ๐ŸŒŠ, understanding iterators is essential for writing elegant, maintainable code.

By the end of this tutorial, youโ€™ll feel confident implementing custom iterators in your own projects! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Iterator Pattern

๐Ÿค” What is the Iterator Pattern?

The Iterator Pattern is like a tour guide ๐Ÿ—บ๏ธ for your data collections. Think of it as a bookmark ๐Ÿ“– that remembers where you are in a collection and helps you move through it step by step.

In TypeScript terms, an iterator provides a standardized way to traverse collections without exposing their internal structure. This means you can:

  • โœจ Access elements sequentially
  • ๐Ÿš€ Support different traversal algorithms
  • ๐Ÿ›ก๏ธ Keep collection internals private

๐Ÿ’ก Why Use Iterator Pattern?

Hereโ€™s why developers love iterators:

  1. Uniform Interface ๐Ÿ”’: Same API for different collections
  2. Separation of Concerns ๐Ÿ’ป: Collection logic stays separate from traversal
  3. Multiple Iterations ๐Ÿ“–: Support multiple simultaneous iterations
  4. Lazy Evaluation ๐Ÿ”ง: Process elements on-demand

Real-world example: Imagine browsing a music playlist ๐ŸŽต. The iterator lets you skip to the next song without knowing how the playlist stores its tracks!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Simple Iterator Implementation

Letโ€™s start with a friendly example:

// ๐Ÿ‘‹ Hello, Iterator Pattern!
interface Iterator<T> {
  hasNext(): boolean;    // ๐Ÿ” Check if more elements exist
  next(): T;            // โžก๏ธ Get next element
  current(): T;         // ๐Ÿ“ Get current element
}

// ๐ŸŽจ Creating a simple collection
class NumberCollection {
  private items: number[] = [];
  
  constructor(items: number[]) {
    this.items = items;
  }
  
  // ๐Ÿš€ Create an iterator
  createIterator(): NumberIterator {
    return new NumberIterator(this.items);
  }
}

// ๐ŸŽฏ The iterator implementation
class NumberIterator implements Iterator<number> {
  private items: number[];
  private index: number = 0;
  
  constructor(items: number[]) {
    this.items = items;
  }
  
  hasNext(): boolean {
    return this.index < this.items.length;
  }
  
  next(): number {
    if (!this.hasNext()) {
      throw new Error("๐Ÿšซ No more elements!");
    }
    return this.items[this.index++];
  }
  
  current(): number {
    return this.items[this.index];
  }
}

๐Ÿ’ก Explanation: The iterator keeps track of position without exposing the array directly. Clean separation! ๐ŸŽฏ

๐ŸŽฏ Using Built-in Iterators

TypeScript supports the ES6 iteration protocol:

// ๐Ÿ—๏ธ Using Symbol.iterator
class PlayList {
  private songs: string[] = [];
  
  addSong(song: string): void {
    this.songs.push(song);
    console.log(`๐ŸŽต Added "${song}" to playlist!`);
  }
  
  // ๐Ÿ”„ Make it iterable
  [Symbol.iterator](): Iterator<string> {
    let index = 0;
    const songs = this.songs;
    
    return {
      next(): IteratorResult<string> {
        if (index < songs.length) {
          return { value: songs[index++], done: false };
        }
        return { value: undefined as any, done: true };
      }
    };
  }
}

// ๐ŸŽฎ Use with for...of
const myPlaylist = new PlayList();
myPlaylist.addSong("TypeScript Rocks");
myPlaylist.addSong("Iterator Groove");

for (const song of myPlaylist) {
  console.log(`๐ŸŽง Now playing: ${song}`);
}

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: Shopping Cart Iterator

Letโ€™s build something real:

// ๐Ÿ›๏ธ Define our product type
interface Product {
  id: string;
  name: string;
  price: number;
  emoji: string;
}

// ๐Ÿ›’ Shopping cart with custom iterator
class ShoppingCart {
  private items: Product[] = [];
  
  // โž• Add product
  addProduct(product: Product): void {
    this.items.push(product);
    console.log(`${product.emoji} Added ${product.name} to cart!`);
  }
  
  // ๐Ÿ’ฐ Get total using iterator
  getTotal(): number {
    let total = 0;
    const iterator = this.createIterator();
    
    while (iterator.hasNext()) {
      const product = iterator.next();
      total += product.price;
    }
    
    return total;
  }
  
  // ๐ŸŽฏ Create custom iterator
  createIterator(): CartIterator {
    return new CartIterator(this.items);
  }
  
  // ๐Ÿ”„ Also support for...of
  [Symbol.iterator](): Iterator<Product> {
    return this.createIterator();
  }
}

// ๐Ÿ“ฆ Cart iterator with filtering
class CartIterator implements Iterator<Product> {
  private items: Product[];
  private position: number = 0;
  
  constructor(items: Product[]) {
    this.items = [...items]; // ๐Ÿ›ก๏ธ Defensive copy
  }
  
  hasNext(): boolean {
    return this.position < this.items.length;
  }
  
  next(): Product {
    if (!this.hasNext()) {
      throw new Error("๐Ÿ›‘ No more items in cart!");
    }
    return this.items[this.position++];
  }
  
  current(): Product {
    return this.items[this.position];
  }
  
  // ๐ŸŽจ Reset to beginning
  reset(): void {
    this.position = 0;
    console.log("๐Ÿ”„ Iterator reset!");
  }
}

// ๐ŸŽฎ Let's shop!
const cart = new ShoppingCart();
cart.addProduct({ id: "1", name: "TypeScript Book", price: 29.99, emoji: "๐Ÿ“˜" });
cart.addProduct({ id: "2", name: "Coffee Mug", price: 12.99, emoji: "โ˜•" });
cart.addProduct({ id: "3", name: "Rubber Duck", price: 9.99, emoji: "๐Ÿฆ†" });

// ๐Ÿ“‹ List items with iterator
console.log("๐Ÿ›’ Cart contents:");
for (const item of cart) {
  console.log(`  ${item.emoji} ${item.name} - $${item.price}`);
}
console.log(`๐Ÿ’ฐ Total: $${cart.getTotal().toFixed(2)}`);

๐ŸŽฏ Try it yourself: Add a filter iterator that only shows items above a certain price!

๐ŸŽฎ Example 2: Game Level Iterator

Letโ€™s make it fun with a game example:

// ๐Ÿ† Game level structure
interface GameLevel {
  id: number;
  name: string;
  difficulty: "easy" | "medium" | "hard";
  stars: number;
  emoji: string;
}

// ๐ŸŽฎ Level collection with multiple iterators
class GameLevelManager {
  private levels: GameLevel[] = [];
  
  // โž• Add level
  addLevel(level: GameLevel): void {
    this.levels.push(level);
    console.log(`${level.emoji} Level "${level.name}" added!`);
  }
  
  // ๐ŸŽฏ Different iteration strategies
  allLevels(): LevelIterator {
    return new LevelIterator(this.levels);
  }
  
  unlockedLevels(starsRequired: number): LevelIterator {
    const unlocked = this.levels.filter(level => level.stars <= starsRequired);
    return new LevelIterator(unlocked);
  }
  
  levelsByDifficulty(difficulty: GameLevel["difficulty"]): LevelIterator {
    const filtered = this.levels.filter(level => level.difficulty === difficulty);
    return new LevelIterator(filtered);
  }
}

// ๐Ÿš€ Advanced iterator with peek functionality
class LevelIterator implements Iterator<GameLevel> {
  private levels: GameLevel[];
  private index: number = 0;
  
  constructor(levels: GameLevel[]) {
    this.levels = levels;
  }
  
  hasNext(): boolean {
    return this.index < this.levels.length;
  }
  
  next(): GameLevel {
    if (!this.hasNext()) {
      throw new Error("๐Ÿ No more levels!");
    }
    const level = this.levels[this.index++];
    console.log(`๐ŸŽฎ Loading level: ${level.emoji} ${level.name}`);
    return level;
  }
  
  current(): GameLevel {
    return this.levels[this.index];
  }
  
  // ๐Ÿ‘€ Peek at next without advancing
  peek(): GameLevel | null {
    if (this.index + 1 < this.levels.length) {
      return this.levels[this.index + 1];
    }
    return null;
  }
  
  // ๐ŸŽฏ Skip to specific level
  skipTo(levelId: number): boolean {
    const newIndex = this.levels.findIndex(l => l.id === levelId);
    if (newIndex !== -1) {
      this.index = newIndex;
      return true;
    }
    return false;
  }
  
  // ๐Ÿ”„ Make it iterable
  [Symbol.iterator](): Iterator<GameLevel> {
    return this;
  }
}

// ๐ŸŽฎ Create game world
const gameManager = new GameLevelManager();
gameManager.addLevel({ id: 1, name: "Tutorial Island", difficulty: "easy", stars: 0, emoji: "๐Ÿ๏ธ" });
gameManager.addLevel({ id: 2, name: "Forest Path", difficulty: "easy", stars: 10, emoji: "๐ŸŒฒ" });
gameManager.addLevel({ id: 3, name: "Mountain Peak", difficulty: "medium", stars: 25, emoji: "๐Ÿ”๏ธ" });
gameManager.addLevel({ id: 4, name: "Dragon's Lair", difficulty: "hard", stars: 50, emoji: "๐Ÿฒ" });

// ๐ŸŒŸ Player with 30 stars
const playerStars = 30;
console.log(`\nโœจ Unlocked levels (${playerStars} stars):`);
const unlockedIterator = gameManager.unlockedLevels(playerStars);
while (unlockedIterator.hasNext()) {
  const level = unlockedIterator.next();
  const nextLevel = unlockedIterator.peek();
  if (nextLevel) {
    console.log(`   Next up: ${nextLevel.emoji} ${nextLevel.name}`);
  }
}

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Generator-Based Iterators

When youโ€™re ready to level up, try generators:

// ๐ŸŽฏ Using generators for cleaner iterators
class MagicCollection<T> {
  private items: T[] = [];
  
  add(item: T): void {
    this.items.push(item);
  }
  
  // ๐Ÿช„ Generator function
  *[Symbol.iterator](): Generator<T> {
    for (const item of this.items) {
      console.log("โœจ Yielding magic item!");
      yield item;
    }
  }
  
  // ๐ŸŒŸ Reverse iterator using generator
  *reverseIterator(): Generator<T> {
    for (let i = this.items.length - 1; i >= 0; i--) {
      yield this.items[i];
    }
  }
  
  // ๐Ÿ’ซ Filter iterator
  *filterIterator(predicate: (item: T) => boolean): Generator<T> {
    for (const item of this.items) {
      if (predicate(item)) {
        yield item;
      }
    }
  }
}

// ๐ŸŽฎ Use the magic!
const spells = new MagicCollection<string>();
spells.add("Fireball ๐Ÿ”ฅ");
spells.add("Ice Shield ๐ŸงŠ");
spells.add("Lightning Bolt โšก");

console.log("๐Ÿช„ Forward iteration:");
for (const spell of spells) {
  console.log(`  Casting: ${spell}`);
}

console.log("\n๐Ÿ”„ Reverse iteration:");
for (const spell of spells.reverseIterator()) {
  console.log(`  Reversing: ${spell}`);
}

๐Ÿ—๏ธ Composite Iterators

For the brave developers:

// ๐Ÿš€ Iterator that combines multiple iterators
class CompositeIterator<T> implements Iterator<T> {
  private iterators: Iterator<T>[];
  private currentIndex: number = 0;
  
  constructor(iterators: Iterator<T>[]) {
    this.iterators = iterators;
  }
  
  hasNext(): boolean {
    while (this.currentIndex < this.iterators.length) {
      if (this.iterators[this.currentIndex].hasNext()) {
        return true;
      }
      this.currentIndex++;
    }
    return false;
  }
  
  next(): T {
    if (!this.hasNext()) {
      throw new Error("๐Ÿ›‘ No more elements in composite!");
    }
    return this.iterators[this.currentIndex].next();
  }
  
  current(): T {
    return this.iterators[this.currentIndex].current();
  }
}

// ๐ŸŽจ Paginated iterator for large datasets
class PaginatedIterator<T> implements Iterator<T[]> {
  private data: T[];
  private pageSize: number;
  private currentPage: number = 0;
  
  constructor(data: T[], pageSize: number = 10) {
    this.data = data;
    this.pageSize = pageSize;
  }
  
  hasNext(): boolean {
    return this.currentPage * this.pageSize < this.data.length;
  }
  
  next(): T[] {
    if (!this.hasNext()) {
      throw new Error("๐Ÿ“„ No more pages!");
    }
    
    const start = this.currentPage * this.pageSize;
    const end = Math.min(start + this.pageSize, this.data.length);
    this.currentPage++;
    
    console.log(`๐Ÿ“– Loading page ${this.currentPage} (items ${start + 1}-${end})`);
    return this.data.slice(start, end);
  }
  
  current(): T[] {
    const start = this.currentPage * this.pageSize;
    const end = Math.min(start + this.pageSize, this.data.length);
    return this.data.slice(start, end);
  }
}

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Modifying Collection During Iteration

// โŒ Wrong way - concurrent modification!
const numbers = new NumberCollection([1, 2, 3, 4, 5]);
const iterator = numbers.createIterator();

while (iterator.hasNext()) {
  const num = iterator.next();
  if (num === 3) {
    numbers.remove(num); // ๐Ÿ’ฅ Danger! Collection modified!
  }
}

// โœ… Correct way - collect changes, apply after
const toRemove: number[] = [];
const safeIterator = numbers.createIterator();

while (safeIterator.hasNext()) {
  const num = safeIterator.next();
  if (num === 3) {
    toRemove.push(num); // ๐Ÿ“ Note for later
  }
}

// ๐Ÿ›ก๏ธ Safe removal after iteration
toRemove.forEach(num => numbers.remove(num));

๐Ÿคฏ Pitfall 2: Not Checking hasNext()

// โŒ Dangerous - might throw error!
function processItems<T>(iterator: Iterator<T>): void {
  for (let i = 0; i < 5; i++) {
    const item = iterator.next(); // ๐Ÿ’ฅ What if only 3 items?
    console.log(item);
  }
}

// โœ… Safe - always check first!
function processItemsSafely<T>(iterator: Iterator<T>): void {
  let count = 0;
  while (iterator.hasNext() && count < 5) {
    const item = iterator.next(); // โœ… Safe now!
    console.log(`${count + 1}. ${item}`);
    count++;
  }
}

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Keep It Simple: Donโ€™t over-engineer iterators for simple collections
  2. ๐Ÿ“ Defensive Copying: Create copies of data to prevent external modifications
  3. ๐Ÿ›ก๏ธ Error Handling: Always check hasNext() before calling next()
  4. ๐ŸŽจ Single Responsibility: Each iterator should have one traversal strategy
  5. โœจ Use Generators: For simpler iterator implementations

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Tree Iterator

Create a type-safe tree iterator that can traverse in different orders:

๐Ÿ“‹ Requirements:

  • โœ… Binary tree structure with generic type
  • ๐Ÿท๏ธ Support in-order, pre-order, and post-order traversal
  • ๐Ÿ‘ค Iterator should be reusable
  • ๐Ÿ“… Add level-order (breadth-first) traversal
  • ๐ŸŽจ Each node needs a value and optional emoji!

๐Ÿš€ Bonus Points:

  • Add a filter option to skip certain values
  • Implement iterator chaining
  • Create a reverse iterator for each traversal type

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
// ๐ŸŽฏ Tree node structure
interface TreeNode<T> {
  value: T;
  emoji?: string;
  left?: TreeNode<T>;
  right?: TreeNode<T>;
}

// ๐ŸŒณ Binary tree with multiple iterators
class BinaryTree<T> {
  private root?: TreeNode<T>;
  
  // โž• Insert value
  insert(value: T, emoji?: string): void {
    this.root = this.insertNode(this.root, value, emoji);
    console.log(`${emoji || "๐ŸŒฟ"} Inserted ${value}`);
  }
  
  private insertNode(node: TreeNode<T> | undefined, value: T, emoji?: string): TreeNode<T> {
    if (!node) {
      return { value, emoji };
    }
    
    if (value < node.value) {
      node.left = this.insertNode(node.left, value, emoji);
    } else {
      node.right = this.insertNode(node.right, value, emoji);
    }
    
    return node;
  }
  
  // ๐ŸŽฏ In-order iterator (left -> root -> right)
  *inOrderIterator(): Generator<T> {
    yield* this.inOrderTraversal(this.root);
  }
  
  private *inOrderTraversal(node?: TreeNode<T>): Generator<T> {
    if (!node) return;
    
    yield* this.inOrderTraversal(node.left);
    console.log(`${node.emoji || "๐Ÿ“"} Visiting: ${node.value}`);
    yield node.value;
    yield* this.inOrderTraversal(node.right);
  }
  
  // ๐Ÿš€ Pre-order iterator (root -> left -> right)
  *preOrderIterator(): Generator<T> {
    yield* this.preOrderTraversal(this.root);
  }
  
  private *preOrderTraversal(node?: TreeNode<T>): Generator<T> {
    if (!node) return;
    
    console.log(`${node.emoji || "๐Ÿ“"} Visiting: ${node.value}`);
    yield node.value;
    yield* this.preOrderTraversal(node.left);
    yield* this.preOrderTraversal(node.right);
  }
  
  // ๐ŸŒŠ Level-order iterator (breadth-first)
  *levelOrderIterator(): Generator<T> {
    if (!this.root) return;
    
    const queue: TreeNode<T>[] = [this.root];
    
    while (queue.length > 0) {
      const node = queue.shift()!;
      console.log(`${node.emoji || "๐ŸŒŠ"} Level visit: ${node.value}`);
      yield node.value;
      
      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }
  }
  
  // ๐ŸŽจ Filtered iterator
  *filterIterator(predicate: (value: T) => boolean): Generator<T> {
    for (const value of this.inOrderIterator()) {
      if (predicate(value)) {
        yield value;
      }
    }
  }
  
  // ๐Ÿ”„ Default iterator
  [Symbol.iterator](): Generator<T> {
    return this.inOrderIterator();
  }
}

// ๐ŸŽฎ Test our tree!
const tree = new BinaryTree<number>();
tree.insert(50, "๐ŸŒณ");
tree.insert(30, "๐ŸŒฟ");
tree.insert(70, "๐Ÿƒ");
tree.insert(20, "๐ŸŒฑ");
tree.insert(40, "๐Ÿ€");
tree.insert(60, "๐ŸŒพ");
tree.insert(80, "๐ŸŒฒ");

console.log("\n๐ŸŽฏ In-order traversal:");
for (const value of tree.inOrderIterator()) {
  console.log(`  Value: ${value}`);
}

console.log("\n๐Ÿš€ Pre-order traversal:");
for (const value of tree.preOrderIterator()) {
  console.log(`  Value: ${value}`);
}

console.log("\n๐ŸŒŠ Level-order traversal:");
for (const value of tree.levelOrderIterator()) {
  console.log(`  Value: ${value}`);
}

console.log("\n๐ŸŽจ Filtered (> 40):");
for (const value of tree.filterIterator(v => v > 40)) {
  console.log(`  Value: ${value}`);
}

๐ŸŽ“ Key Takeaways

Youโ€™ve learned so much! Hereโ€™s what you can now do:

  • โœ… Create custom iterators with confidence ๐Ÿ’ช
  • โœ… Use Symbol.iterator for built-in iteration support ๐Ÿ›ก๏ธ
  • โœ… Implement generators for cleaner code ๐ŸŽฏ
  • โœ… Handle edge cases safely ๐Ÿ›
  • โœ… Build traversal strategies for any collection! ๐Ÿš€

Remember: Iterators provide a clean, consistent way to traverse collections without exposing internals! ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered the Iterator Pattern!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Practice with the tree iterator exercise
  2. ๐Ÿ—๏ธ Add iterators to your existing collections
  3. ๐Ÿ“š Explore the Visitor Pattern for processing iterated elements
  4. ๐ŸŒŸ Share your custom iterator implementations!

Remember: Every iteration brings you closer to mastery. Keep coding, keep learning, and most importantly, have fun! ๐Ÿš€


Happy coding! ๐ŸŽ‰๐Ÿš€โœจ