Home > OS >  Discriminated type unions in typescript with lookup object
Discriminated type unions in typescript with lookup object

Time:07-07

I have some event interface that might have different type signature, depending on what type of event it is. Let's say we have 'FizzChange' and 'BuzzChange', and they both extend 'EventTypeBase'

interface EventTypeBase {
  id: string;
  timestamp: string;
}

interface FizzChange extends EventTypeBase {
  name: 'fizz';
  payload: string;
}

interface BuzzChange extends EventTypeBase {
  name: 'buzz';
  payload: number;
}

Now that we defined it, we can use it in function, and with conditional statement check what type we are working with

const mapEventsWithIfs = (event: EventType) => {
  // This works, typescript can see what type we want to use
  if (event.name === 'fizz') {
    return event.payload; // Typescript can see that this is a string
  }

  if (event.name === 'buzz') {
    return event.payload; // And this is a number
  }

  return;
}

What i'm trying to achieve, is that instead of if statements, I want to use a lookup object, where each type gets its own handler function, but that doesn't work. How can I type lookup object, in a way that will work?

Here is a link to reproduce it

const mapEvents = (event: EventType) => {
  // This doesn't work, event gets reduces to never
  // Argument of type 'EventType' is not assignable to parameter of type 'never'.
  //   The intersection 'FizzChange & BuzzChange' was reduced to 'never'
  //   because property 'name' has conflicting types in some constituents.
  //     Type 'FizzChange' is not assignable to type 'never'.(2345)
  return EventHandlersMap[event.name](event);
}

CodePudding user response:

Add fizz and buzz as type.

type ActionType = 'fizz' | 'buzz';

In EventHandlersMap give type to your object. So typescript will understand the object.

const EventHandlersMap: { [K in ActionType]: (event: ExampleEvent & any) => void } = {
    fizz: (event: FizzChange) => event.payload,
    buzz: (event: BuzzChange) => event.payload
}

CodePudding user response:

Here's a somewhat involved approach that uses the discriminated union mapped type approach described here along with a generic constraint:

interface EventTypeBase {
  id: string;
  timestamp: string;
}

interface FizzChange extends EventTypeBase {
  name: 'fizz';
  payload: string;
}

interface BuzzChange extends EventTypeBase {
  name: 'buzz';
  payload: number;
}

type DiscriminateUnion<T, K extends keyof T, V extends T[K]>
  = T extends Record<K, V> ? T : never;

type MapDiscriminatedUnion<T extends Record<K, string>, K extends keyof T>
  = { [V in T[K]]: DiscriminateUnion<T, K, V> };

type EventType = FizzChange | BuzzChange;

type EventMap = MapDiscriminatedUnion<EventType, 'name'>;

const eventHandlersMap: {
  [K in keyof EventMap]: (event: EventMap[K]) => EventMap[K]['payload'];
} = {
  'fizz': (event: FizzChange) => event.payload,
  'buzz': (event: BuzzChange) => event.payload
}

const mapEvents = <K extends keyof EventMap>(event: EventMap[K]) => {
  const name: K = event.name;
  return eventHandlersMap[name](event);
}

Playground link

  • Related