制作一个类型良好的通用事件到处理程序分配器函数

时间:2019-06-25 22:39:24

标签: typescript types functional-programming typescript-generics

这是this exploration的延续,指出了一种可重用的机制,该机制使我们可以将传入的事件(消息)分配给适当的事件处理程序,并在此过程中完全依赖于类型。这就是我们想要的可重用性:

const handleEvent = 
  <EventKind extends keyof EventsMap>
  (e: Event<EventKind>): Promise<void> => {
  const kind: EventKind = e.kind;
  const handler = <(e: CrmEvent<EventKind>) => Promise<void>>handlers[kind]; // Notice the seemingly unnecessary assertion. This is the reason we are making this function generic.
  return handler(e);
};

我希望我们理想地在这里结束:

const handleEvent = eventAssigner<CrmEventsMap>(handlers, 'kind');

这一切都始于将事件鉴别符与事件主体相关联的映射:

interface CrmEventsMap {
  event1: { attr1: string,  attr2: number }
  event2: { attr3: boolean, attr4: string }
}

从中,我们可以创建完整的事件类型(其中包括鉴别符):

type CrmEvent<K extends keyof CrmEventsMap> = { kind: K } & EventsMap[K]

我们现在拥有声明处理程序映射所需的一切:

const handlers: { [K in keyof CrmEventsMap]: (e: CrmEvent<K>) => Promise<void> } = {
  event1: ({attr1, attr2}) => Promise.resolve(),
  event2: ({attr3, attr4}) => Promise.resolve(),
};

这使我们回到handleEvent。体内的类型断言似乎足以尝试使该函数具有泛型性。

这是尝试:

const eventAssigner =
  <EventMap extends {},
    EventKind extends keyof EventMap,
    KindField extends string>
  (
    handlers: { [k in keyof EventMap]: (e: EventType<EventMap, k, KindField>) => any },
    kindField: KindField
  ) =>
    (e: EventType<EventMap, EventKind, KindField>):
      ReturnType<(typeof handlers)[EventKind]> => {
      const kind = e[kindField];
      const handler = <(e: EventType<EventMap, EventKind, KindField>) => ReturnType<(typeof handlers)[EventKind]>>handlers[kind];
      return handler(e);
    };

type EventType<EventMap extends {}, Kind extends keyof EventMap, KindField extends string> =
  { [k in KindField]: Kind } & EventMap[Kind]

即使在用法上也很复杂。但是,仅通过将事件识别符字段固定到'kind',我们就可以大大简化事情:

const eventAssigner =
  <EventMap extends {},
    EventKind extends keyof EventMap>
  (handlers: { [k in keyof EventMap]: (e: EventType<EventMap, k>) => any }) =>
    (e: EventType<EventMap, EventKind>):
      ReturnType<(typeof handlers)[EventKind]> =>
      handlers[e.kind](e);

type EventType<EventMap extends {}, Kind extends keyof EventMap> = { kind: Kind } & EventMap[Kind]

这一章特别有趣的是,由于某种原因,我无法解释,我们不需要类型断言。

为使这两个函数中的任何一个起作用,都需要为它们提供具体的类型参数,这意味着将它们包装在另一个函数中:

const handleEvent = 
  <E extends CrmEventKind>
  (e: CrmEvent<E>): ReturnType<(typeof handlers)[E]> => 
    eventAssigner<CrmEventMap, E>(handlers)(e);

因此,简而言之,您认为我们可以达到理想的实现程度多远?

Here's a playground.

1 个答案:

答案 0 :(得分:1)

几次打自己的脑袋以了解这里发生的事情后,我有所收获。

首先,我建议稍微松开handlers的类型,以免要求,使处理程序参数具有"kind"判别式,如下所示:

interface CrmEventMap {
  event1: { attr1: string; attr2: number };
  event2: { attr3: boolean; attr4: string };
}

const handlers: {
  [K in keyof CrmEventMap]: (e: CrmEventMap[K]) => Promise<void>
} = {
  event1: ({ attr1, attr2 }) => Promise.resolve(),
  event2: ({ attr3, attr4 }) => Promise.resolve()
};

因此,您这里根本不需要CrmEvent<K>。您最终的handleEvent实现将需要使用判别式来告知如何调度事件,但是上述handlers无关紧要:每个函数仅对已经适当调度的事件起作用。您可以根据需要将上述内容保持不变,但这对我来说似乎是不必要的。

现在要实施eventAssigner

const eventAssigner = <
  M extends Record<keyof M, (e: any) => any>,
  D extends keyof any
>(
  handlers: M,
  discriminant: D
) => <K extends keyof M>(
  event: Record<D, K> & (Parameters<M[K]>[0])
): ReturnType<M[K]> => handlers[event[discriminant]](event);

因此,eventAssigner是一个经过咖喱的泛型函数。它在M中是通用的,handlers的类型(您可以作为变量handlers使用)必须是具有一元参数函数属性的对象,并且D discriminant的类型(您以字符串"kind"的形式),它必须是有效的密钥类型。然后,它返回另一个在K中通用的函数,该函数旨在作为M的键之一。其event参数的类型为Record<D, K> & (Parameters<M[K]>[0]),这基本上意味着它必须与K的{​​{1}}键属性相同的类型参数,以及具有判别式的对象键M和值D。这是您的K类型的类似物。

它返回CrmEvent<K>。该实现不需要类型声明,仅因为对ReturnType<M[K]>的约束具有每个处理函数扩展了M。因此,当编译器检查(e: any)=>any时,会看到一个必须可分配给handlers[event[discriminant]]的函数,您基本上可以在任何参数上调用它并返回任何类型。因此,它很乐意让您返回(e: any)=>any。因此,您在这里需要小心。您可以省去handlers[event[discriminant]]("whoopsie") + 15并使用类似any的方法,这会更安全,但是您必须使用类型断言。由你决定。

无论如何,这是您的用法:

(e: never)=>unknown

请注意,您只是在使用泛型类型推断,而不必在其中指定类似const handleEvent = eventAssigner(handlers, "kind"); 之类的内容。在我看来,使用类型推断更为有效 “理想”要比手动指定事物更重要。如果要在此处指定某些内容,则必须为<CrmEventsMap>,这很愚蠢。

并确保其行为符合预期:

eventAssigner<typeof handlers, "kind">(handlers, "kind")

看起来不错。好的,希望对您有所帮助。祝你好运!

Link to code