网站搜索

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 链接