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
206: Typing Async Iterators
Async iterators are one of those features that feel like magic when they work, but the moment you try to implement a custom one without a generator function, the TypeScript types start fighting you. I see this most often when developers try to wrap a legacy callback-based API or a paginated REST endpoint into something that can be used with a for await...of loop.
The "Implicit Any" Iterator Trap
Imagine you're building a MessageQueue reader. You want to be able to stream messages from a server one by one. You might start writing something like this:
interface Message {
id: string;
text: string;
}
class MessageQueue implements AsyncIterable<Message> {
private cursor = 0;
private messages = ['Hello', 'World', 'TypeScript is cool'];
async *[Symbol.asyncIterator]() {
while (this.cursor < this.messages.length) {
yield { id: `${this.cursor}`, text: this.messages[this.cursor++] };
}
}
}
// This works because of the async generator (*) syntax...
// But what happens when you can't use a generator?
Async generators (the async * syntax) are great because TypeScript handles the heavy lifting for you. But in the real world, you'll eventually encounter a situation where you have to implement the iterator manually—perhaps because you're integrating with a complex state machine or a third-party library that doesn't support generators. When you do that, you'll likely hit a wall like this:
class ManualMessageQueue implements AsyncIterable<Message> {
private cursor = 0;
private messages = ['Hello', 'World', 'TypeScript is cool'];
[Symbol.asyncIterator]() {
return {
async next() {
if (this.cursor < this.messages.length) {
return { value: { id: '1', text: '...' }, done: false };
}
return { done: true };
}
};
}
}
If you hover over that return object, you'll notice TypeScript is complaining. It's telling you that the object you're returning doesn't satisfy the AsyncIterator<Message> interface. Specifically, it's confused about the return type of next(). You've returned a promise that resolves to an object, but the interface is very picky about the shape of that result.
Aligning with AsyncIteratorResult
The core of the problem is that AsyncIterable is just a wrapper that tells TypeScript: "this object has a method that returns an AsyncIterator." The AsyncIterator itself is what actually does the work via the next() method.
To fix the typing, you need to be explicit about the IteratorResult<T>. The next() method must return a Promise<IteratorResult<T>>. If you leave it implicit, TypeScript often struggles to infer that the done: true case is compatible with the done: false case when wrapped in a Promise.
Here is how we clean that up. I prefer defining the iterator return type explicitly to avoid any ambiguity:
class ManualMessageQueue implements AsyncIterable<Message> {
private cursor = 0;
private messages = ['Hello', 'World', 'TypeScript is cool'];
// We explicitly tell TS that this returns an AsyncIterator
[Symbol.asyncIterator](): AsyncIterator<Message> {
return {
async next(): Promise<IteratorResult<Message>> {
if (this.cursor < this.messages.length) {
return {
value: { id: `${this.cursor}`, text: this.messages[this.cursor++] },
done: false
};
}
// We must return the object with done: true to signal the end of the loop
return { value: undefined, done: true };
}
};
}
}
async function run() {
const queue = new ManualMessageQueue();
for await (const msg of queue) {
console.log(msg.text);
}
}
Notice two key things here:
- The return type of
[Symbol.asyncIterator]isAsyncIterator<Message>. - The return type of
next()isPromise<IteratorResult<Message>>.
I'll give you a pro tip: if you find yourself writing a lot of manual AsyncIterator implementations, you're probably doing it the hard way. Always check if an async * generator can do the job first. But when it can't, knowing that IteratorResult is the bridge between your logic and the for await...of loop is what saves you from a debugging nightmare.
📋 Practical Task
Build a Paginated API Streamer
You are tasked with creating a UserStreamer class that implements AsyncIterable<User>. Instead of using an async generator, you must implement the AsyncIterator manually to practice the typing we just covered.
Requirements:
- Define a
Userinterface withid: numberandusername: string. - Create a
UserStreamerclass that holds an internalcurrentPagecounter. - Implement
[Symbol.asyncIterator]()so that it returns an object with anext()method. - The
next()method should simulate an API call (use aPromisewithsetTimeoutorasync/await). - The streamer should stop (return
done: true) oncecurrentPagereaches 3. - Ensure all return types are explicitly typed using
AsyncIteratorandIteratorResult.
interface User {
id: number;
username: string;
}
class UserStreamer implements AsyncIterable<User> {
private currentPage = 1;
// Your implementation here
}
// Test your code:
async function test() {
const streamer = new UserStreamer();
for await (const user of streamer) {
console.log(`Fetched user: ${user.username}`);
}
}
There are no comments for now.