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
205: Typing Generator Functions
Generators are one of those features that often sit in the "I know they exist, but I'm not sure when to actually use them" category. But once you start dealing with large datasets or complex state machines, they're a lifesaver. The tricky part is that TypeScript's Generator type takes three generic arguments, and if you've never seen them, it looks like a bit of a riddle.
Let's build a batchProcessor. Imagine we have a massive list of user IDs that we need to process in chunks to avoid hitting API rate limits. This is a perfect spot for a generator because we don't want to slice the whole array into memory upfront; we want to yield chunks on demand.
Drafting the basic batcher
I'll start by sketching out the logic. I want to pass in an array and a batch size, and then yield a slice of that array every time .next() is called.
function* batchProcessor<T>(items: T[], batchSize: number) {
for (let i = 0; i < items.length; i += batchSize) {
yield items.slice(i, i + batchSize);
}
}
TypeScript is actually pretty smart here. If I don't provide an explicit return type, it infers it. But in a professional codebase, we shouldn't rely on inference for the public API of a utility function. We need to be explicit about what this generator produces, what it returns when it's finished, and what it accepts if we send values back into it.
Defining the Generator generic
The Generator type looks like this: Generator<T, TReturn, TNext>. Here is how I break that down in my head:
- T: What the generator
yields(the value you get from.next().value). - TReturn: What the generator
returnswhen it finally finishes (the final.next().valuewhendoneis true). - TNext: What the generator receives when you call
.next(value).
For our batcher, we are yielding arrays of T, we aren't returning anything special at the end, and we aren't sending any values back in. So, it should look like this:
function* batchProcessor<T>(items: T[], batchSize: number): Generator<T[], void, unknown> {
for (let i = 0; i < items.length; i += batchSize) {
yield items.slice(i, i + batchSize);
}
}
Correcting a common return type slip
Here is where I usually make a mistake when I'm rushing. I'll try to return the total number of batches processed at the end of the function to be "helpful," but I'll forget to update the TReturn type.
// This will throw a TypeScript error!
function* batchProcessor<T>(items: T[], batchSize: number): Generator<T[], void, unknown> {
let count = 0;
for (let i = 0; i < items.length; i += batchSize) {
count++;
yield items.slice(i, i + batchSize);
}
return count; // Error: Type 'number' is not assignable to type 'void'.
}
I just tried to return a number, but I told TypeScript the return type was void. To fix this, I need to change that second generic argument from void to number. It's a small change, but it's the difference between your code compiling and a frustrating morning of debugging.
function* batchProcessor<T>(items: T[], batchSize: number): Generator<T[], number, unknown> {
let count = 0;
for (let i = 0; i < items.length; i += batchSize) {
count++;
yield items.slice(i, i + batchSize);
}
return count;
}
Handling the TNext argument
Now, let's get fancy. What if we want to be able to "skip" a batch by sending a signal back into the generator? We can use the TNext parameter. Instead of unknown, let's say we want to send a boolean to indicate if we should double the batch size for the next iteration.
function* batchProcessor<T>(
items: T[],
batchSize: number
): Generator<T[], number, boolean> {
let count = 0;
let currentSize = batchSize;
for (let i = 0; i < items.length; i += currentSize) {
count++;
// The value passed into .next() becomes the result of the yield expression
const shouldDouble = yield items.slice(i, i + currentSize);
if (shouldDouble) {
currentSize *= 2;
}
}
return count;
}
Now the generator is fully typed. It yields T[], returns a number, and accepts a boolean. This is a powerful pattern for creating bidirectional communication between the caller and the iterator.
📋 Practical Task
Exercise: Build a Paginated API Response Simulator
Your task is to create a generator function called paginateUsers that simulates fetching users from a database in pages. The generator should meet the following requirements:
- Input: An array of usernames (strings) and a
pageSize(number). - Yield: It should yield a page of usernames (an array of strings).
- Next: It should accept a
booleanvia.next(). Iftrue, the generator should skip the next page entirely. Iffalse(or undefined), it proceeds as normal. - Return: When the generator is exhausted, it should return the total number of pages actually yielded.
Make sure you explicitly type the Generator return type using the Generator<T, TReturn, TNext> generic.
There are no comments for now.