Skip to Content
Course content

204: Typing Custom Iterable Classes

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Most of the time, you're just iterating over arrays or maps. It's easy. But as you build more complex domain models, you'll hit a point where you want your own custom class to behave like a native collection. You want to be able to use for...of loops or the spread operator directly on your object without having to call some clumsy method like deck.getCards().forEach(...).

Building the Deck structure

Let's build a Deck class. I want this deck to hold a set of cards, and I want to be able to iterate over them. I'll start with a simple type for the cards and a class to manage them.

type Card = { suit: string; rank: string };

class Deck {
  private cards: Card[] = [];

  addCard(card: Card) {
    this.cards.push(card);
  }
}

const myDeck = new Deck();
myDeck.addCard({ suit: 'Hearts', rank: 'A' });
myDeck.addCard({ suit: 'Spades', rank: 'K' });

Hitting the 'Not Iterable' Wall

Now, naturally, I want to loop through my deck. I'll try to use a for...of loop because it's clean.

for (const card of myDeck) {
  console.log(card.rank);
}

TypeScript immediately flags this with an error: "Type 'Deck' is not an iterable type." This is because, to TypeScript (and JavaScript), myDeck is just an object. It has no idea how to "step through" its contents. To fix this, we need to implement the Iterable interface.

Implementing the Iterable Interface

To make this work, I need to do two things: tell TypeScript the class implements Iterable<Card>, and provide the [Symbol.iterator] method. This method is the "secret handshake" that JavaScript looks for when you use a for...of loop.

class Deck implements Iterable<Card> {
  private cards: Card[] = [];

  addCard(card: Card) {
    this.cards.push(card);
  }

  [Symbol.iterator]() {
    // I'll just return the iterator from the internal array
    return this.cards[Symbol.iterator]();
  }
}

Now the loop works! But this is a bit boring. What if I want to control how the iteration happens? Maybe I only want to iterate over "Face Cards" (J, Q, K).

The Generator Shortcut (and my little slip-up)

The easiest way to create a custom iterator is using a generator function (the ones with the *). I'll rewrite the iterator to filter out any card that isn't a face card.

[Symbol.iterator]* () {
  for (const card of this.cards) {
    if (['J', 'Q', 'K'].includes(card.rank)) {
      yield card;
    }
  }
}

Wait, I just caught myself making a classic mistake. I wrote the logic, but I forgot that by changing the behavior of the iterator, I've fundamentally changed what the Deck "is" when iterated. If I use this Deck in other parts of my app expecting all cards, I'm now missing data.

I realize that making the class itself Iterable usually implies iterating over the entire collection. If I want a filtered version, I shouldn't put that logic in [Symbol.iterator]. Instead, I should keep the main iterator simple and create a separate method that returns a generator for the filtered view.

Here is the corrected, professional approach:

class Deck implements Iterable<Card> {
  private cards: Card[] = [];

  addCard(card: Card) {
    this.cards.push(card);
  }

  // The standard iterator: returns everything
  [Symbol.iterator](): Iterator<Card> {
    return this.cards[Symbol.iterator]();
  }

  // A custom generator method for specific views
  *faceCards() {
    for (const card of this.cards) {
      if (['J', 'Q', 'K'].includes(card.rank)) {
        yield card;
      }
    }
  }
}

Now I have the best of both worlds: for (const card of myDeck) gives me the whole deck, and for (const card of myDeck.faceCards()) gives me only the royalty. This keeps the class predictable while remaining flexible.




📋 Practical Task

Build a PaginatedResult Iterable

Imagine you are building a wrapper for an API that returns data in pages. You want to be able to loop through all the items across all pages as if they were one big list, without manually managing the page offsets in your business logic.

Your Task:

  • Create a type User = { id: number; name: string }.
  • Create a class PaginatedUserList that implements Iterable<User>.
  • The class should take an array of "pages" in its constructor (where each page is an array of User).
  • Implement the [Symbol.iterator] method using a generator (*) that yields every user from every page sequentially.
  • Instantiate the class with a few pages of users and use a for...of loop to print every user's name to the console.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.