-
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
173: Practice Exercise: Building a Typed Command Bus
A few years ago, I was reviewing a pull request for a colleague who was building a complex order processing system. They had implemented a "Command Bus" to decouple the API controllers from the business logic. It looked clean at first glance, but the core method was essentially execute(command: any): Promise<any>. I remember the dread I felt when I realized that if someone renamed a property in the ProcessPaymentCommand, the compiler wouldn't say a word. The system would just crash at runtime when the handler tried to access a field that didn't exist. It was a classic case of using TypeScript as a suggestion rather than a guarantee.
The goal of a typed command bus is to create a strict contract between a command (the "what") and its handler (the "how"). We want the TypeScript compiler to know that if you pass a CreateUserCommand into the bus, the bus is internally routing it to a handler that specifically expects that command and returns a specific result—like a UserId—without us having to cast types manually every time we call the bus.
Linking Commands to their Results
To make this work, we can't just use a generic Command interface. We need a way to map the command type to its expected return type. I usually do this by defining a registry—a type map—that acts as the single source of truth for the entire application. Instead of the bus guessing what happens, we tell it explicitly: "When this command is sent, this is the type that comes back."
type CommandRegistry = {
CreateUserCommand: { response: UserId; payload: { name: string; email: string } };
DeleteUserCommand: { response: void; payload: { id: UserId } };
};
By structuring our registry this way, we can use indexed access types to extract the response and payload. This removes the need for any or unknown and lets us leverage the power of TypeScript's type system to enforce consistency across our handler implementations.
Implementing the Type-Safe Dispatcher
The real magic happens in the dispatch method. You want a method that accepts a command key and its associated payload, then returns the specific response type mapped in your registry. The trick is using a generic parameter that extends the keys of your registry. If you try to pass a command key that doesn't exist in the map, or a payload that doesn't match the definition for that specific key, the compiler will stop you immediately.
I've found that the cleanest way to implement the internal handler lookup is to use a Map where the keys are the command identifiers and the values are the handler instances. However, because the handlers have different input and output types, you'll likely have to use a small amount of type assertion inside the dispatch method itself. This is a professional trade-off: we use a narrow "unsafe" area inside the bus implementation to provide a 100% type-safe public API for the rest of the team. It's much better to have one as any inside a utility class than a thousand as any calls scattered throughout your business logic.
📋 Practical Task
Exercise: Implementing a Type-Safe Inventory Command Bus
Your task is to build a typed Command Bus for a warehouse inventory system. You need to ensure that the dispatch method is fully type-safe based on a provided registry.
Requirements:
- Define a
CommandRegistrytype that mapsAddItemCommand(returnsitemId: string) andRemoveItemCommand(returnssuccess: boolean). - Create a
CommandHandlerinterface that takes a payload and returns a response. - Implement a
CommandBusclass with adispatch<K extends keyof CommandRegistry>method. - The
dispatchmethod must take the command key and the correct payload for that key, and return the correct response type defined in the registry. - Implement a simple mock handler for
AddItemCommandand register it within the bus.
Test your implementation: Try to call bus.dispatch('AddItemCommand', { wrongField: 123 }). If the TypeScript compiler doesn't throw an error, your mapping isn't strict enough!
There are no comments for now.