这是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);
因此,简而言之,您认为我们可以达到理想的实现程度多远?
答案 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")
看起来不错。好的,希望对您有所帮助。祝你好运!