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

Posted

技术标签:

【中文标题】制作一个类型精美的通用事件到处理程序分配器函数【英文标题】:Make a nicely typed generic event-to-handler assigner function 【发布时间】:2019-11-07 19:32:57 【问题描述】:

这是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.

【问题讨论】:

嗯,我觉得这个问题有点难读。当前实现的完整副本会有所帮助。我尝试将代码片段复制粘贴到 TS 游乐场,但无法解决。 @NiiloKeinänen 感谢您指出这一点。添加了一个游乐场,但也修复了问题中的几个错误。 进一步改进实现是指缩短“handleEvent”和“eventAssigner”的定义,还是改进调用“handleEvent”的接口?目前我们需要做:handleEvent( kind: 'event1', attr1: 'hello', attr2: 0 ) 如果事件签名是唯一的,我想能够省略 'kind' 属性是合乎逻辑的。 @NiiloKeinänen kind 是必要的,因为它是唯一能完全区分一个事件与另一个事件的字段。我的目标是“改进调用eventAssigner 的界面”,使其看起来尽可能接近:eventAssigner&lt;CrmEventsMap&gt;(handlers, 'kind') 【参考方案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&lt;K&gt;。您最终的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),它必须是一个拥有单参数函数属性的对象,Ddiscriminant 的类型(它你有作为字符串"kind"),它必须是一个有效的密钥类型。然后它返回另一个在K 中通用的函数,旨在成为M 的键之一。它的event 参数是Record&lt;D, K&gt; &amp; (Parameters&lt;M[K]&gt;[0]) 类型,这基本上意味着它必须是与MK-keyed 属性相同的类型参数,以及具有判别键D 和值@987654348 的对象@。这是您的 CrmEvent&lt;K&gt; 类型的模拟。

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

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

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

请注意,您只是在使用泛型类型推断,而不必在其中指定类似 &lt;CrmEventsMap&gt; 的任何内容。在我看来,使用类型推断更多 “理想”而不是手动指定事物。如果你想在这里指定一些东西,它必须是eventAssigner&lt;typeof handlers, "kind"&gt;(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

【讨论】:

太美了,眼泪都快掉下来了。我不希望可以省略事件地图类型。感谢您加倍努力! 如果可能的话,请推荐一些文献来帮助我将所有这些与类型相关的问题放入我的直觉中。我想像写代码一样写类型。要么,要么我需要继续打扰你: 同时,开始你自己的书吧! (我已经试了几个,但他们很快就进入了打字课,这是我睡着的时候) 不知道有没有什么可以推荐的。我从TypeScript handbook 开始(并且有一个新版本正在开发中here),但在那之后我花了很多时间阅读GitHub issues。 我设法从我的应用程序中表示一个用例,其中eventAssigner 不接受该事件。 Here's the code。请在proxyEventAssigner 函数中查找问题。 (由于 url 大小,无法使用 Playground) 我不知道除了“使用类型断言”之外我是否能够解决这个问题或想出一个更好的答案,所以如果你想问一个新问题希望有更大的机会关注这一点。

以上是关于制作一个类型精美的通用事件到处理程序分配器函数的主要内容,如果未能解决你的问题,请参考以下文章

打字稿函数类型分配问题

不能分配值类型

无法在 mongoose 中使用通用模型:“x”类型的参数不可分配给 MongooseFilterQuery 类型的参数

如何处理对象属性内的事件函数?

FreeRTOS - 资源如何使用分配

具有通用返回类型的打字稿函数