typescript - 如何为严格类型的事件触发器使用相关联合类型
我正在实现一个自定义事件发射器模块,这需要在泛型类型参数中定义可能的事件名称和有效负载。
用法如下:
interface MyEventPayloadMapping = {
event1: string;
event2: void;
}
const emitter = createEventEmitter<MyEventPayloadMapping>();
emitter.on('event1', (payload: string) => {});
emitter.on('event2', () => {});
EventEmitter 的类型定义如下:
type PayloadOneArg<T> = undefined extends T ? [T?] : [T];
type PayloadArgs<T> = T extends undefined ? [] : PayloadOneArg<T>; // allow zero, one optional or exactly one argument
type Listener<Arguments> = (...args: PayloadArgs<Arguments>) => void;
interface EventEmitter<T> {
on<K extends keyof T>(eventName:K, listener: Listener<T[K]>): void;
}
现在,当我使用多个 EventEmitter 实例并且它们的事件负载映射部分重叠时,我遇到了一个问题。
interface PayloadA {
readonly foo: number;
readonly bar: string;
}
interface PayloadB {
readonly foo: number;
readonly baz: boolean;
}
type EmitterA = EventEmitter<PayloadA>;
type EmitterB = EventEmitter<PayloadB>;
interface ModuleA {
readonly type: 'a';
readonly on: EmitterA['on'];
}
interface ModuleB {
readonly type: 'b';
readonly on: EmitterB['on'];
}
function doStuff(mod: ModuleA | ModuleB): void {
if (mod.type === 'a') {
mod.on('foo', () => {}); // works
} else {
mod.on('foo', () => {}); // works
}
mod.on('foo', () => {}); // doesn’t work
}
这会产生错误
This expression is not callable.
Each member of the union type '(<K extends keyof PayloadA>(eventName: K, listener: Listener<PayloadA[K]>) => void) | (<K extends keyof PayloadB>(eventName: K, listener: Listener<PayloadB[K]>) => void)' has signatures, but none of those signatures are compatible with each other.
在 TypeScript 项目中创建问题后,有人指出在这个拉取请求中已经实现了解决此问题的方法。我对这个拉取请求的描述理解有些困难,也不太清楚如何将这个概念应用到我的使用场景中。
我的第一次尝试如下:
type EventNameToListenerMap<T> = { [P in keyof T]: { name: P, listener: Listener<T[P]>} }
interface EventEmitter<T> {
on<K extends keyof T>(eventName: EventNameToListenerMap<T>[K]['name'], listener: EventNameToListenerMap<T>[K]['listener']): void;
}
但我仍然收到相同的错误。这是一个 playground 链接。
所以问题是,有人能解释一下 TypeScript 拉取请求中提出的解决方案的概念吗?另外,也许还能提供一些提示,说明它如何应用到我的使用场景中?
答案#1
对此类事情的一般方法,如 microsoft/TypeScript#47109 中所述,是制作一些简单的映射接口,例如
interface PayloadMap {
a: PayloadA,
b: PayloadB
}
然后将你的操作表示为一个针对键类型参数 K 的通用函数,该参数受限于该映射接口的键,并且这个函数的参数表示为一个 可分配对象类型,该类型根据这个映射接口定义。像这样:
type Module<K extends keyof PayloadMap = keyof PayloadMap> = { [P in K]:
{ readonly type: P, readonly on: OnEventEmitter<PayloadMap[P]> }
}[K]
我在这里将 OnEventEmitter 定义为
interface OnEventEmitter<T> {
<P extends keyof T>(
eventName: P,
listener: (...args: PayloadArgs<T[P]>) => void
): void;
}
Module<K> 类型是一个分布式对象类型,因为它是一个映射类型,我们会立即对其进行索引。当 K 是 "a" 时,你得到的是 ModuleA 类型,而当 K 是 "b" 时,你得到的是 ModuleB 类型。重要的是,单独的 Module 类型因此正好对应于你的原始 ModuleA | ModuleB 联合类型。
无论如何,现在你可以编写 doStuff() 并且它可以编译了:
function doStuff<K extends keyof PayloadMap>(mod: Module<K>): void {
mod.on('foo', (x) => { x.toFixed() }); // okay
}
即使输入是 ModuleA 和 ModuleB 的联合,你也可以调用它:
declare const moduleA: Module<"a">
declare const moduleB: Module<"b">
doStuff(Math.random() < 0.5 ? moduleA : moduleB);
doStuff() 的实现能够工作是因为 mod.on() 被视为一个 单一 泛型函数类型 <"foo">(eventName: "foo", listener: (...args: PayloadArgs<PayloadMap[K]["foo"]>) => void) => void。如果 doStuff() 不是泛型,或者如果 Module<K> 被表达为对编译器不透明的某种类型(例如 Extract<ModuleA | ModuleB, {type: K}>),那么你在尝试调用 mod.on() 时就会出现编译错误:
// Don't do this
type OpaqueModule<K extends keyof PayloadMap> =
Extract<Module<"a"> | Module<"b">, { type: K }>
function doOpaqueStuff<K extends keyof PayloadMap>(mod: OpaqueModule<K>): void {
mod.on('foo', (x) => { x.toFixed() }); // error
// none of those signatures are compatible with each other
}
代码的 Playground 链接
