制作一个类型良好的通用事件到处理程序分配器函数
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);
所以简而言之,您认为我们能离理想的实施有多近?
在多次敲打自己的脑袋以了解这里发生的事情之后,我得到了一些东西。
首先,我建议稍微放宽 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 函数属性的对象,并且 D
,discriminant
的类型(字符串 "kind"
)必须是有效的键类型。然后它 return 是 K
中通用的另一个函数,旨在成为 M
的键之一。它的 event
参数是 Record<D, K> & (Parameters<M[K]>[0])
类型,这基本上意味着它必须是与 M
的 K
键控 属性 相同类型的参数,以及一个具有判别键 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>
看起来不错。好的,希望有帮助。祝你好运!
这是
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);
所以简而言之,您认为我们能离理想的实施有多近?
在多次敲打自己的脑袋以了解这里发生的事情之后,我得到了一些东西。
首先,我建议稍微放宽 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 函数属性的对象,并且 D
,discriminant
的类型(字符串 "kind"
)必须是有效的键类型。然后它 return 是 K
中通用的另一个函数,旨在成为 M
的键之一。它的 event
参数是 Record<D, K> & (Parameters<M[K]>[0])
类型,这基本上意味着它必须是与 M
的 K
键控 属性 相同类型的参数,以及一个具有判别键 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>
看起来不错。好的,希望有帮助。祝你好运!