In the beginning, the basic calling signature for waitEvent()
is quite simplistic:
declare function waitEvent<K extends string, A extends any[]>(emitter: {
once(event: K, cb: (...args: A) => void): void
}, event: K): Promise<A>;
When using waitEvent()
on an emitter with a compatible once()
method, the compiler can infer the type of A
:
const thing = {
once(event: "loaded", cb: (title: string) => void) { }
}
const [title] = await waitEvent(thing, "loaded");
title // string
const otherThing = {
once(event: "exploded", cb: (blastRadius: number) => void) { }
}
const [rad] = await waitEvent(otherThing, "exploded");
rad // number
However, this approach falls short in real-world scenarios where the callback parameters are interrelated and cannot be represented by simple types like the examples shown above.
We need to combine multiple call signatures into a single object that can handle different ways of invoking once()
.
To express this relationship in TypeScript, one common approach is using overloads with multiple call signatures:
interface MyEventEmitter {
once(event: "loaded", cb: (title: string) => void): void;
// ... more ... //
once(event: "exploded", cb: (blastRadius: number) => void): void;
}
This strategy is adopted by libraries like "playwright" when directly calling once()
on a value of type MyEmitter
:
declare const emitter: MyEventEmitter;
emitter.once("loaded", title => title.toUpperCase()); // okay
emitter.once("exploded", rad => rad.toFixed(2)); // okay
But TypeScript struggles to determine the type of cb
based on the event
parameter unless explicitly called.
This limitation hampers the functionality of methods like waitEvent()
, as seen in the code snippet below:
const [title] = await waitEvent(emitter, "loaded");
title // number ?!!?!
Various workarounds exist, but they come with limitations and are not entirely elegant solutions. Hardcoding the expected type of emitter
in a customized version of waitEvent()
might be the most practical option until TypeScript's support for higher-order functions improves.
Another alternative would involve making once()
generic in a mapping interface, providing some advantages but still suffering from limitations similar to the overloaded version.
Despite its shortcomings, this representation allows inspection and possible revisions to waitEvent
as demonstrated in the following example:
declare function waitEvent<
A extends [string, (...args: any) => void],
K extends A[0]
>(emitter: {
once(...args: A): void
}, event: K): Promise<Parameters<Extract<A, [K, any]>[1]>>;
const [title] = await waitEvent(emitter, "loaded");
title // string
const [rad] = await waitEvent(emitter, "exploded");
rad // number
Ultimately, understanding the nature of emitter
and creating a specialized version of waitEvent()
tailored to that type seems to be the most viable approach given the current constraints of TypeScript.