Patterns
How do I keep each event name paired with the right payload and listener?
Use an event map and one key parameter so the event name selects the payload type in both methods.
The recipe
type Handler<Events, K extends keyof Events> = (payload: Events[K]) => void type TypedEmitter<Events extends object> = { on<K extends keyof Events>(event: K, handler: Handler<Events, K>): void emit<K extends keyof Events>(event: K, payload: Events[K]): void } type Events = { saved: { id: string } failed: { message: string } } declare const bus: TypedEmitter<Events> bus.on('saved', payload => payload.id) bus.emit('failed', { message: 'offline' })
The build compiles this and checks each result below.
How it works
- 01
K extends keyof Events
The key can only be a declared event name.
- 02
payload: Events[K]
Indexed access turns that chosen name into its exact payload.
What you get
keyof Events extends 'saved' | 'failed' ? ('saved' | 'failed' extends keyof Events ? true : false) : false
→trueParameters<Handler<Events, 'saved'>>[0]→{ id: string; }Parameters<Handler<Events, 'failed'>>[0]→{ message: string; }
Where it goes wrong
The type only protects calls that pass through this interface. A loosely typed emitter underneath can still deliver malformed runtime data, so validate events that cross process or network boundaries.
Takeaway
A map plus `K extends keyof Map` is the standard way to keep a name correlated with its data.
