One issue with the
interface EventListener<TEvent extends keyof Events = keyof Events> {
event: TEvent;
handler: (...args: Parameters<Events[TEvent]>) => void;
}
lies in how, when you use EventListener
without specifying a type argument, it defaults to the union type of keyof Events
. As a result, both event
and handler
end up being linked to this union type independently.
The ideal scenario is for EventListener<TEvent>
to distribute over unions within TEvent
, so that EventListener<X | Y>
translates to
EventListener<X> | EventListener<Y>
. This means
EventListener
(with no type argument) should itself be a union. Since interfaces cannot be unions, it has to be converted into a
type
alias.
There are different approaches to achieve this. One option is using a distributive conditional type to ensure a union input results in a union output:
type EventListener<TEvent extends keyof Events = keyof Events> =
TEvent extends any ? {
event: TEvent;
handler: (...args: Parameters<Events[TEvent]>) => void;
} : never;
type E = EventListener
/* type E = {
event: Event.ItemCreated;
handler: (item: string) => void;
} | {
event: Event.UserUpdated;
handler: (user: number) => void;
} */
If there's no need to explicitly define the type argument, you can simplify by making EventListener
a specific union type. This involves refactoring it as a distributive object type where you index into a mapped type to obtain the desired union:
type EventListener = { [TEvent in keyof Events]: {
event: TEvent,
handler: (...args: Parameters<Events[TEvent]>) => void
} }[keyof Events];
/* type EventListener = {
event: Event.ItemCreated;
handler: (item: string) => void;
} | {
event: Event.UserUpdated;
handler: (user: number) => void;
} */
By doing this, EventListener
becomes a discriminated union, allowing the compiler to utilize event
as a discriminant property to refine the handler
property as needed:
const UserListener: EventListener = {
event: Event.UserUpdated,
handler: (handlerArgs) => {
handlerArgs.toFixed(2); // valid
console.log(handlerArgs);
},
};
Playground link to code