In my opinion, the best approach would be to create a utility interface that connects the event name (as a key) with the corresponding list of argument types (as a value):
interface EventArgs {
event1: [number, string, string];
event2: [string, string];
// ...
}
After defining this interface, you can then customize your addEventListener()
and emit()
methods by using generics in K
, where the type of the e
argument is constrained to be a key within 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've named the interface Foo
for reference, as it wasn't specified initially in the query. Assuming you have an implementation that creates a Foo
instance:
const foo: Foo = makeFoo();
You can see how it functions as intended:
// Successful calls
foo.addEventListener("event1", (n, s1, s2) => {
console.log(n.toFixed(2), s1.toUpperCase(), s2.toLowerCase()); // works fine
});
foo.emit("event2", "Abc", "Def"); // works fine
foo.emit("event1", Math.PI, "Abc", "Def"); // works fine
// Unsuccessful calls
foo.emit("event1", "Abc", "Def"); // error!
foo.addEventListener("event2", (n, s1, s2) => { // error!
console.log(n.toFixed(2), s1.toUpperCase(), s2.toLowerCase())
});
This essentially addresses the question concerning typings. Here's one way to implement it with a good level of type safety:
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;
}
This implementation holds an object map that associates event names with arrays of event listeners. It adds the listener to the correct array when using addEventListener
and invokes the listeners from the appropriate array in the emit
method.
Link to code on TypeScript Playground