TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
204: Typing Custom Iterable Classes
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
PaginatedUserListthat implementsIterable<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...ofloop to print every user's name to the console.
There are no comments for now.