Hello I am facing problem when trying to implement types for TinyEmitter. I need to implement 2 methods. First:
addEventListener(e: string, (...args: any[]) => void): void;
Second:
emit(e: string, ...args: any[]): void;
But this solution is not suggesting event names, and parameters.
I need to define event types, and event arguments. Something like:
type EventMap = [
(e: 'event1', arg1: number, arg2: string, arg3: string) => void,
(e: 'event2', arg1: string, arg2: string) => void,
...
];
Actually i can infer event name by:
type EventParam<I extends number = number> = Parameters<EventMap[I]>[0];
this will infer event type ('event1' | 'event2')
For other parameters i tried:
type EventArgs<I extends number = number> = EventMap[I] extends ((e: EventParam<I>, ...args: infer P) => any ? P : never;
how to implement this for addEventListener
and emit
functions with type suggestions ?
Thank you in advance for any advice.
The approach I'd recommend is to make a utility interface which maps the name of the event (as a key) to the corresponding list of argument types (as a value):
interface EventArgs {
event1: [number, string, string];
event2: [string, string];
// ...
}
Then you can define your addEventListener()
and emit()
methods as being generic in K
, the type of the e
argument constrained to be a key of EventArgs
:
interface Foo {
addEventListener<K extends keyof EventArgs>(e: K, cb: (...args: EventArgs[K]) => void): void;
emit<K extends keyof EventArgs>(e: K, ...args: EventArgs[K]): void;
}
I'm calling the interface Foo
since it wasn't specified in the question. Then assuming we have an implementation that makes a Foo
:
const foo: Foo = makeFoo();
We can see that it behaves as desired:
// Good calls
foo.addEventListener("event1", (n, s1, s2) => {
console.log(n.toFixed(2), s1.toUpperCase(), s2.toLowerCase()); // okay
});
foo.emit("event2", "Abc", "Def"); // okay
foo.emit("event1", Math.PI, "Abc", "Def"); // okay
// Bad calls
foo.emit("event1", "Abc", "Def"); // error!
foo.addEventListener("event2", (n, s1, s2) => { // error!
console.log(n.toFixed(2), s1.toUpperCase(), s2.toLowerCase())
});
That essentially answers the question which is primarily about typings; still, it can be implemented like this in a fairly type-safe way:
function makeFoo(): Foo {
const listenerMap: { [K in keyof EventArgs]?: ((...args: EventArgs[K]) => void)[] } = {}
const ret: Foo = {
addEventListener<K extends keyof EventArgs>(e: K, cb: (...args: EventArgs[K]) => void) {
const listeners: ((...args: EventArgs[K]) => void)[] = listenerMap[e] ??= [];
listeners.push(cb);
},
emit<K extends keyof EventArgs>(e: K, ...a: EventArgs[K]) {
const listeners: ((...args: EventArgs[K]) => void)[] = listenerMap[e] ?? [];
listeners.forEach(cb => cb(...a))
}
}
return ret;
}
Essentially it just holds onto an object map from event names to arrays of event listeners, and addEventListener
pushes the listener onto the right array (initializing it first if need be), while emit
calls the listeners from the right array (unless it doesn't exist).