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
99: Typing Iterators and Iterables
A few years ago, I was reviewing a PR for a data-processing library. A colleague had built this QueryResults class that wrapped a large array of database records. To get the data, you had to call results.getItems() and then loop over that array. It wasn't a dealbreaker, but it felt clunky. I suggested making the QueryResults class itself iterable so we could just use for (const row of results). He tried it, but he didn't type the iterator correctly. Suddenly, the row variable inside the loop became any. We lost all our type safety for the most critical part of the data pipeline, and we didn't even notice until a runtime error hit production because someone assumed a field existed that didn't.
Most of the time, you're just iterating over arrays or maps, and TypeScript handles that magic behind the scenes. But when you start building custom collections or complex data streams, you need to tell TypeScript exactly what's coming out of your iterator to keep those type guarantees intact.
Implementing the Iterable Protocol
To make an object iterable in TypeScript, you have to implement the Iterable<T> interface. This basically tells the compiler, "This object has a method that can produce an iterator." That method is always [Symbol.iterator]. If you forget this, or if you type it as a generic function, you'll end up with the any problem I mentioned earlier.
Take a look at this ProductCatalog example. Instead of exposing the internal array, we make the catalog itself iterable:
interface Product {
id: string;
name: string;
price: number;
}
class ProductCatalog implements Iterable<Product> {
private products: Product[] = [
{ id: '1', name: 'Mechanical Keyboard', price: 150 },
{ id: '2', name: 'Ergonomic Mouse', price: 80 },
{ id: '3', name: '4K Monitor', price: 400 },
];
// This is the magic method that allows for...of loops
[Symbol.iterator](): Iterator<Product> {
let index = 0;
const products = this.products;
return {
next(): IteratorResult<Product> {
if (index < products.length) {
return { value: products[index++], done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
}
const catalog = new ProductCatalog();
for (const product of catalog) {
console.log(product.name); // TypeScript knows 'product' is a Product
}
Distinguishing the Iterable from the Iterator
It's easy to confuse Iterable and Iterator, but they are distinct roles. I like to think of the Iterable as the "book" and the Iterator as the "bookmark." The book is the thing you can read, but the bookmark is the object that actually knows where you are in the process and how to get to the next page.
- Iterable<T>: An object that has a
[Symbol.iterator]method. Its only job is to return an iterator. - Iterator<T>: An object with a
next()method that returns anIteratorResult<T>(which is just a fancy name for{ value: T, done: boolean }).
If you're using generator functions (the ones with the *), TypeScript simplifies this immensely because generators automatically implement both interfaces. If you find yourself writing the next() logic manually—like I did in the ProductCatalog example—you're doing it the hard way. I usually only do that when I need extremely fine-grained control over the iteration state or when I'm avoiding generators for specific performance reasons in a tight loop.
📋 Practical Task
Building a Type-Safe Circular Buffer Iterator
You are tasked with creating a CircularBuffer class that stores a fixed number of integers. When the buffer is full, new elements overwrite the oldest ones. To make this buffer useful for other developers, you need to implement the Iterable<number> interface so the buffer can be used directly in a for...of loop.
Requirements:
- Create a class
CircularBufferthat accepts acapacity: numberin its constructor. - Implement a
push(value: number)method to add items to the buffer. - Implement
[Symbol.iterator]()so that iterating over the buffer yields the elements in the order they were added (oldest to newest), regardless of where they are stored internally in the array. - Ensure that the loop variable in a
for...ofloop is strictly typed as anumber.
Starter Code:
class CircularBuffer implements Iterable<number> {
private buffer: number[] = [];
private pointer = 0;
constructor(private capacity: number) {}
push(value: number) {
// Your implementation here
}
[Symbol.iterator](): Iterator<number> {
// Your implementation here
}
}There are no comments for now.