I am trying to type my event API as such:
type Apple = {
seeds: number;
}
type Peach = {
weight: number;
}
interface AddAppleEvent {
apple: Apple;
}
interface AddPeachEvent {
peach: Peach;
}
interface EventMap {
"addApple": AddAppleEvent;
"addPeach": AddPeachEvent;
}
class EventApi{
on<K extends keyof EventMap>(type: K, listener: (ev: EventMap[K]) => any): void{
}
}
const api = new EventApi();
api.on("addApple", (evt) => {
console.log(evt.apple);
});
This works wonder: Typescript knows what is in my evt variable from the name of the event I am registering on.
But: I would like ot be able to register to an array of events, and type the result (which will be a composition of the types of each events):
api.on(["addApple", "addPeach"], ({apple, peach}) => {
if(apple) {
// do something
}
if(peach) {
// do something else
}
})
Unfortunately I cant find a way to iterate over an array of keys from an interface to build an union of all the values referred.
Any idea ?
Here is a link to the typescript playground with the example above
-----EDIT----
Just summing up the great answers I got for this post:
Thanks a lot guys:)
I'd start by defining EventMapKey
just to avoid retyping keyof EventMap
:
type EventMapKey = keyof EventMap;
Then it depends on whether you want a union or an intersection.
You can define onMultiple
like this:
onMultiple<K extends EventMapKey[], EventType extends EventMap[K[number]]>(type: K, listener: (ev: EventType) => any): void {
console.log("on add multiple event listenner", listener);
}
Playground link - I added a third event type (just to be sure), and the example usage of onMultiple
at the end works:
api.onMultiple(["addApple", "addPeach"], (evt) => {
// Here, evt is AddAppleEvent | AddPeachEvent
});
Naturally, before you can use the apple
or peach
properties of evt
, you have to do a type guard to check what kind of event you're dealing with. To aid with that, I'd probably add a type
to the event types so you can work from that:
interface AddAppleEvent {
type: "addApple";
apple: Apple;
}
interface AddPeachEvent {
type: "addPeach";
peach: Peach;
}
api.onMultiple(["addApple", "addPeach"], (evt) => {
if (evt.type === "addApple") {
console.log(evt.apple.seeds);
} else {
console.log(evt.peach.weight);
}
});
But you can also work from "apple" in evt
without adding type
if you like.
api.onMultiple(["addApple", "addPeach"], (evt) => {
if ("apple" in evt) {
console.log(evt.apple.seeds);
} else {
console.log(evt.peach.weight);
}
});
If you want an intersection instead, here's the solution jcalz posted in a comment:
type ComposedEventListener<K extends EventMapKey[]> = {
[I in keyof K]: (ev: EventMap[Extract<K[I], EventMapKey>]) => any
}[number] extends (ev: infer I) => any ? (ev: I) => any : never
class EventApi {
on<K extends EventMapKey>(type: K, listener: (ev: EventMap[K]) => any): void {
console.log("on add event listenner", listener);
}
// here, write K as a set of keys from EventMap allows to pass multiple keys,
// but I cant find how to type the listener properlly.
onMultiple<K extends EventMapKey[]>(type: readonly [...K], listener: ComposedEventListener<K>): void {
console.log("on add multiple event listenner", listener);
}
}
const api = new EventApi();
api.on("addApple", (evt) => {
console.log(evt.apple);
});
api.onMultiple(["addApple", "addPeach"], (evt) => { });