I need a way to type an object, where the key is the value of the 'event' field of a specific type, and the value is an array of callbacks that takes an object of the same type's data subtype.
I have tried using mapped types, but I am a beginner with typescript and really struggling with this.
// I have this type structure, where the event is always a string, but the data can be anything (but is constrained by the event)
interface EventTemplate {
event: string;
data: any;
}
export interface CreateEvent extends EventTemplate {
event: 'create_game';
data: {
websocketID: 'string';
};
}
export interface JoinEvent extends EventTemplate {
event: 'join_game';
data: {
gameID: 'string';
};
}
export interface MessageEvent extends EventTemplate {
event: 'message';
data: string;
}
export type WSEvent = CreateEvent | JoinEvent | MessageEvent;
// I want an object like this
type callbacks = {
[key in WSEvent['event']]: ((data: WSEvent['data']) => void)[];
};
// Except that it forces the data structure to match with the key used. IE using a specific WSEvent rather than a generic one
// Something along the lines of:
type callbacks = {
[key in (T extends WSEvent)['event']]: ((data: T['data']) => void)[];
};
// ...only valid..
const callbacks: callbacks = {
// So this should be valid:
message: [(data: MessageEvent['data']): void => {}, (data: MessageEvent['data']): void => {}],
// But this should not be valid, as CreateEvent doesn't have the event 'join_game'
join_game: [(data: CreateEvent['data']): void => {}],
};
I am happy to restructure any of the above if it helps.
What we essentially need is a way to look up the type of the whole event by providing the event name. This can be accomplished using a conditional helper type
type EventByName<E extends WSEvent['event'], T = WSEvent> = T extends {event: E} ? T : never;
The first generic argument E
must be one of the event names. The second one is the union type we're trying to narrow down. It defaults to WSEvent
so there's no need to specify it. The conditional expression then only returns those events in the union type which extend {event: E}
(where E
is the event name).
Once we have the helper type it's pretty easy to adjust your existing mapped type for the callbacks accordingly:
type Callbacks = {
[E in WSEvent['event']]: ((data: EventByName<E>['data']) => void)[];
};
Playground
Sidenote regarding the name of callbacks
. It's recommended to use PascalCase for types. It makes it easier to distinguish from variables. I've changed it in my example to Callbacks
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With