23

Making the switch => object Pattern Work in TypeScript | Max's Dev B...

 4 years ago
source link: https://maxburson.com/switch-object-pattern-with-typescript/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

October 08, 2019

The switch => object pattern is great

I'm a huge fan of the JavaScript pattern where one replaces a switch:

function handle(event) {
  switch (event.kind) {
    case 'goblins_are_coming':
      repelMonsters(event);
    case 'family_is_coming':
      stockFridge(event);
  }
}

with an object:

const eventHandlers = {
  goblins_are_coming: repelMonsters,
  family_is_coming: stockFridge,
};

The second method really shines when we don't want to specify the kinds of events we are handling in the same file.

import familyEventHandlers from './familyEventHandlers';

export default const eventHandlers = {
  goblins_are_coming: repelMonsters,
  ...familyEventHandlers,
};

The pattern doesn't work in TypeScript very well

Assume this is written above the rest of the TypeScript examples:

interface GoblinEvent {
  kind: 'goblins_are_coming';
  smell: number;
}

interface FamilyEvent {
  kind: 'family_is_coming';
  notice: number;
}

type OurEvent = GoblinEvent | FamilyEvent;

declare const repelMonsters: (event: GoblinEvent) => void;
declare const stockFridge: (event: FamilyEvent) => void;

TypeScript plays fine with the first JavaScript example.

function handle(event: FamilyEvent | GoblinEvent) {
  switch (event.kind) {
    case 'goblins_are_coming':
      repelMonsters(event);
    case 'family_is_coming':
      stockFridge(event);
  }
}

But can't make sense of the second one.

const eventHandlers = {
  goblins_are_coming: repelMonsters,
  family_is_coming: stockFridge,
};

declare const e: OurEvent;

eventHandlers[e.kind](e);

This ☝ produces this πŸ‘‡ error message.

Argument of type 'OurEvent' is not assignable to parameter of type 'GoblinEvent & FamilyEvent'.
  Type 'GoblinEvent' is not assignable to type 'GoblinEvent & FamilyEvent'.
    Property 'notice' is missing in type 'GoblinEvent' but required in type 'FamilyEvent'.ts(2345)

Here's the great (terrible?) hack to make it work

function handle<P extends OurEvent, T extends P['kind']>(
  payload: P,
  stuff: { [K in T]: (arg0: P) => void },
) {
  stuff[payload.kind](payload);
}

handle(e, eventHandlers);

No type errors! 🎊

... but with a catch.

This works because the interface is still generic when accessed. To keep this compiling, the object must preserve it's mapped typings as long as the event is generic.

That is to say, the following produces a type error:

function handleWrapper<P extends OurEvent>(payload: P) {
  handle(payload, eventHandlers);
}

TLDR:

Check this out:

function handle<P extends OurEvent, T extends P['kind']>(
  payload: P,
  stuff: { [K in T]: (arg0: P) => void },
) {
  stuff[payload.kind](payload);
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK