Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to specify union types as object keys Typescript

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.

like image 560
JForster Avatar asked Jan 25 '23 21:01

JForster


1 Answers

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.

like image 115
lukasgeiter Avatar answered Jan 28 '23 10:01

lukasgeiter