-
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
238: Typing an Event Bus with Overloaded Emit
When you're building a decoupled system, an event bus is a lifesaver. But if you've tried building one in TypeScript, you've probably run into the "any" trap. You want a single emit method that can handle ten different event types, each with a different payload. If you just type the payload as any, you've basically turned off TypeScript, which defeats the whole purpose of using it.
Why can't I just use a generic record for the event payloads?
You technically can, but it gets messy fast. If you use a generic emit<T>(event: string, data: T), the compiler doesn't actually know which T belongs to which event string. You'd have to manually pass the type every time you call the function, like bus.emit<PlayerPayload>('PLAYER_MOVE', data). That's tedious and error-prone.
Overloads allow us to create a strict mapping. We tell TypeScript: "If the first argument is 'USER_LOGIN', the second argument must be a User object." This gives you that sweet, automatic autocomplete in your IDE without you having to explicitly pass generic types at the call site.
How do I actually structure the emit overloads?
The trick is to list your specific signatures first, and then provide one general "implementation signature" that handles the logic. Let's look at a game state example. I want a bus that handles player movement and game-over events.
interface GameEvents {
'PLAYER_MOVE': { x: number; y: number };
'PLAYER_LEVEL_UP': { level: number; xp: number };
'GAME_OVER': { winner: string };
}
class EventBus {
// Overload 1: Player Move
emit(event: 'PLAYER_MOVE', data: GameEvents['PLAYER_MOVE']): void;
// Overload 2: Level Up
emit(event: 'PLAYER_LEVEL_UP', data: GameEvents['PLAYER_LEVEL_UP']): void;
// Overload 3: Game Over
emit(event: 'GAME_OVER', data: GameEvents['GAME_OVER']): void;
// The implementation signature (this is where the actual code lives)
emit(event: string, data: any): void {
console.log(`Emitting ${event} with data:`, data);
// Logic to notify listeners goes here
}
}
const bus = new EventBus();
bus.emit('PLAYER_MOVE', { x: 10, y: 20 }); // Works!
bus.emit('PLAYER_MOVE', { level: 5 }); // Error: Type '{ level: number }' is not assignable to...
Why does the implementation signature use 'any'?
This is where a lot of people get tripped up. In TypeScript, the "implementation signature" (the last one with the actual function body) is not visible to the outside world. It's only used to ensure that the function body can handle all the overloads defined above it.
Since your emit method needs to be flexible enough to accept a User, a Score, or a Boolean, the implementation signature has to be broad. Using any or unknown here is perfectly fine—and actually necessary—because the strict type checking happens at the overload level before the code ever hits the function body.
Can I avoid writing out every single overload by hand?
If you have 50 events, writing 50 overloads is a nightmare. I've been there. While function overloads are great for a small, fixed set of events, if your event list is huge, you should shift to using a generic constraint combined with a lookup interface.
Instead of multiple emit lines, you can do this:
class EventBus {
emit<K extends keyof GameEvents>(event: K, data: GameEvents[K]): void {
console.log(`Emitting ${event}`, data);
}
}
This achieves the exact same type safety as overloads but in one line. I usually recommend overloads when the logic for different events might diverge significantly, but for a standard "pass this data to that listener" bus, the generic lookup is the cleaner way to go.
📋 Practical Task
Exercise: Type-Safe Analytics Event Bus
You are building an analytics module for an e-commerce site. You need to implement an AnalyticsBus that ensures tracking events are sent with the correct metadata.
Requirements:
- Define an interface
AnalyticsEventswith three events:'ADD_TO_CART': payload should be{ productId: string, price: number }'PURCHASE': payload should be{ transactionId: string, total: number }'PAGE_VIEW': payload should be{ url: string, referrer: string }
- Create an
AnalyticsBusclass with anemitmethod. - Use function overloads to ensure that calling
emit('ADD_TO_CART', { ... })only accepts the product ID and price. - Implement the
emitmethod body to simply log the event and data to the console. - Test your implementation by attempting to emit a
'PURCHASE'event withPAGE_VIEWdata to confirm TypeScript throws a compile-time error.
There are no comments for now.