I'm attempting to make an emit
function that can accept multiple arguments. Furthermore, the 2nd argument and beyond will be validated by TypeScript based on the 1st argument (the event).
I have the code below as a starting point, but it obviously doesn't work as intended.
type CallbackFunc<T extends any[]> = (...args: T) => void;
interface TrackablePlayerEventMap {
'play:other': CallbackFunc<['audio' | 'video']>;
error: CallbackFunc<[Error, Record<string, unknown>]>;
}
const eventHandlers: Partial<TrackablePlayerEventMap> = {};
function on<K extends keyof TrackablePlayerEventMap>(event: K, callback: TrackablePlayerEventMap[K]) {
}
function emit<K extends keyof TrackablePlayerEventMap>(event: K, ...args: TrackablePlayerEventMap[K]) {
const handler = eventHandlers[event];
handler(...args);
}
on('error', (e, metadata) => {
console.log(e, metadata)
});
on('play:other', x => {
console.log(x);
});
// This should be valid
emit('error', new Error('Ouch'), { count: 5 });
// This should be invalid
emit('error', 123, 456);
CodePudding user response:
In order to write handler(...args)
once inside the body of emit()
and have it compile with no errors, you need to make use of the technique introduced in microsoft/TypeScript#47109 as a fix for an issue I've called "correlated unions", as mentioned in microsoft/TypeScript#30581.
The first step is to come up with a type that maps your event
values to the parameters list (the args
array). It's like your TrackablePlayerEventMap
type, but the properties are just parameter tuple types and not function types:
interface TPEventParamsMap {
'play:other': ['audio' | 'video'],
error: [Error, Record<string, unknown>];
}
Then we give eventHandlers
type a type written explicitly in terms of TPEventParamsMap
:
const eventHandlers: {
[K in keyof TPEventParamsMap]?: CallbackFunc<TPEventParamsMap[K]>
} = {};
Conceptually that's the exact same as your Partial<TrackablePlayerEventMap>
type, but this one is explicitly written as a mapped type on TPEventParamsMap
, which the compiler can use to keep track of the correlation between the event
type (of type K
) and the parameters to the event handler function (of type TPEventParamsMap[K]
).
Continuing:
function emit<K extends keyof TPEventParamsMap>(
event: K,
...args: TPEventParamsMap[K]
) {
const handler = eventHandlers[event];
handler?.(...args); // okay
}
That compiles with no error. Note that since eventHandlers
is Partial
it might not have a value at key event
, so we are using the optional chaining operator (?.
) to call the function only if it exists.
And emit()
behaves as desired from the caller's side too:
emit('error', new Error('Ouch'), { count: 5 }); // okay
emit('error', 123, 456); // error, 123 is not an Error