While exploring different approaches to create a type-safe event emitter, I came across a pattern where you start by defining your event names and their corresponding types in an interface, as shown below:
interface UserEvents {
nameChanged: string;
phoneChanged: number;
}
The 'on' method is then designed to accept a keyof UserEvents and a callback function that takes a parameter of the type associated with that key using lookup types - UserEvents[keyof T]:
function on<T extends keyof UserEvents>(e: T, cb: (val: UserEvents[T]) => void) {/**/}
This setup enables type-safe method calls as demonstrated below:
on('nameChanged', (val) => {}); // callback parameter inferred as string
on('phoneChanged', (val) => {}); // ... inferred as number
However, I encountered an issue within the 'on' method implementation where I struggled to narrow down the type of the provided callback based on the passed key value. Despite attempting to perform type checks against the key type, I found that the callback type remained untouched, merging both possible types:
function on<T extends keyof UserEvents>(e: T, cb: (_: UserEvents[T]) => void) {
if (e === 'nameChanged') {
// expected cb type: (string) => void
} else {
// should be: (number) => void
}
// unfortunately, the cb type inside this function resolves to: (string & number) => void
}
Is there a way to automatically infer the callback type based on the event key using TypeScript features like type guards or discriminated unions while maintaining the specific method call signature?