Iterator

Traverse elements of a collection without exposing its underlying representation. Whether the collection is an array, tree, graph, or custom structure, the iterator provides a uniform way to walk through it.

The Problem

You have a Playlist that internally stores songs. Sometimes it should play in order, sometimes shuffle, sometimes only songs with certain tags. If you expose the internal songs array, callers become coupled to the implementation. Switching from an array to a linked list or a database cursor breaks all callers. And implementing three traversal modes inside Playlist violates the Single Responsibility Principle.


The Solution

Extract the traversal into an Iterator object. The collection returns an iterator; the iterator knows how to walk through the data in a specific way. The collection can return different iterators for different traversal strategies.

«interface» Iterable + [Symbol.iterator]() «interface» Iterator<T> + next(): IteratorResult + done: boolean ConcreteCollection - items: T[] + [Symbol.iterator]() ConcreteIterator - cursor: number + next() creates
Iterator — Collection creates a ConcreteIterator; client only sees the Iterator interface

Real-World Analogy

A TV remote’s channel buttons. You press Next without knowing how many channels there are, what order they’re in, or whether they’re stored in a list or a database. The remote (iterator) abstracts the traversal — you just say “next.”


TypeScript Example

class Song {
  constructor(
    readonly title: string,
    readonly artist: string,
    readonly genre: string,
    readonly durationSec: number,
  ) {}

  toString(): string {
    return `"${this.title}" by ${this.artist} [${this.genre}]`;
  }
}

// Sequential iterator
class SequentialIterator implements globalThis.Iterator<Song>, Iterable<Song> {
  private index = 0;

  constructor(private readonly songs: Song[]) {}

  next(): IteratorResult<Song> {
    if (this.index < this.songs.length) {
      return { value: this.songs[this.index++], done: false };
    }
    return { value: undefined as unknown as Song, done: true };
  }

  [Symbol.iterator]() { return this; }
}

// Shuffle iterator — Fisher-Yates on the fly
class ShuffleIterator implements Iterable<Song> {
  private shuffled: Song[];
  private index = 0;

  constructor(songs: Song[]) {
    this.shuffled = [...songs];
    for (let i = this.shuffled.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [this.shuffled[i], this.shuffled[j]] = [this.shuffled[j], this.shuffled[i]];
    }
  }

  [Symbol.iterator](): globalThis.Iterator<Song> {
    return this.shuffled[Symbol.iterator]();
  }
}

// Filtered iterator — wraps another iterator
class GenreIterator implements Iterable<Song> {
  constructor(
    private readonly songs: Song[],
    private readonly genre: string,
  ) {}

  [Symbol.iterator](): globalThis.Iterator<Song> {
    return this.songs.filter(s => s.genre === this.genre)[Symbol.iterator]();
  }
}

// Collection — returns different iterators
class Playlist {
  private songs: Song[] = [];

  add(song: Song): this {
    this.songs.push(song);
    return this;
  }

  // Different traversal strategies via different iterators
  sequential(): Iterable<Song>              { return new SequentialIterator(this.songs); }
  shuffled(): Iterable<Song>                { return new ShuffleIterator(this.songs); }
  byGenre(genre: string): Iterable<Song>    {
    return { [Symbol.iterator]: () => this.songs.filter(s => s.genre === genre)[Symbol.iterator]() };
  }
}

// Usage — client code uses for...of, never touches the internal array
const playlist = new Playlist();
playlist
  .add(new Song('Bohemian Rhapsody', 'Queen', 'rock', 354))
  .add(new Song('Lose Yourself', 'Eminem', 'hiphop', 326))
  .add(new Song('Hotel California', 'Eagles', 'rock', 391))
  .add(new Song('Stairway to Heaven', 'Led Zeppelin', 'rock', 482))
  .add(new Song('Lose Control', 'Missy Elliott', 'hiphop', 218));

console.log('--- In order ---');
for (const song of playlist.sequential()) {
  console.log(' ', song.toString());
}

console.log('\n--- Rock only ---');
for (const song of playlist.byGenre('rock')) {
  console.log(' ', song.toString());
}

console.log('\n--- Shuffled ---');
for (const song of playlist.shuffled()) {
  console.log(' ', song.toString());
}

When to Use

  • You want to traverse a collection without exposing its internal structure
  • You need multiple traversal strategies for the same collection
  • You want a uniform interface for traversing different collection types
  • Built-in in modern languages: JavaScript’s for...of and Symbol.iterator, Python’s __iter__, Java’s Iterable

Pros

  • Single Responsibility — traversal logic lives in the iterator, not the collection
  • Open/Closed — add new iterator types without changing the collection
  • Multiple iterators can traverse the same collection simultaneously (each has its own state)
  • Provides a uniform interface for diverse data structures

Cons

  • Overkill for simple arrays that already have built-in iteration
  • Stateful iterators can be awkward if the collection changes during iteration

  • Composite trees are commonly traversed with iterators
  • Factory Method creates the right iterator for a collection
  • Visitor uses iteration internally to apply an operation to each element
  • Memento can snapshot an iterator’s position to restore later