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

Make a nicely typed generic event-to-handler assigner function

这是 的延续,它提出了一种可重用的机制,让我们可以将传入的事件(消息)分配给适当的事件处理程序,并在此过程中完全依赖于类型。以下是我们想要使其可重复使用的内容:

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.

在多次敲打自己的脑袋以了解这里发生的事情之后,我得到了一些东西。

首先,我建议稍微放宽 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)必须是一个包含 one-argument 函数属性的对象,并且 Ddiscriminant 的类型(字符串 "kind")必须是有效的键类型。然后它 return 是 K 中通用的另一个函数,旨在成为 M 的键之一。它的 event 参数是 Record<D, K> & (Parameters<M[K]>[0]) 类型,这基本上意味着它必须是与 MK 键控 属性 相同类型的参数,以及一个具有判别键 D 和值 K 的对象。这是您的 CrmEvent<K> 类型的模拟。

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

无论如何,这是你如何使用它的:

const handleEvent = eventAssigner(handlers, "kind");

请注意,您只是在使用泛型类型推断,不必在其中指定 <CrmEventsMap> 之类的任何内容。在我看来,使用类型推断更多 "ideal" 比手动指定的东西。如果你想在这里指定一些东西,它必须是 eventAssigner<typeof handlers, "kind">(handlers, "kind"),这很愚蠢。

并确保它的行为符合您的预期:

const event1Response = handleEvent({ kind: "event1", attr1: "a", attr2: 3 }); // Promise<void>
const event2Response = handleEvent({ kind: "event2", attr3: true, attr4: "b" }); // Promise<void>

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

Link to code