[英]How to use correlated union types for a strictly typed event emitter
我正在實現一個自定義事件發射器模塊,它需要在通用類型參數中定義可能的事件名稱和有效負載。
用法如下所示:
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;
}
但我仍然得到同樣的錯誤。 這是一個游樂場鏈接。
那么問題來了,有人能解釋一下 typescript 拉取請求引入的解決方案的概念嗎? 也許還提供一些提示如何將其應用於我的用例?
如microsoft/TypeScript#47109中所述,此類事情的一般方法是制作一些簡單的映射接口,例如
interface PayloadMap {
a: PayloadA,
b: PayloadB
}
然后將您的操作表示為通用function 上的鍵類型參數K
約束到該映射接口的鍵,並且此 function 的參數表示為分布 ZA8CFDE6331BD4B62AC96F8911 中定義的映射接口類型。 像這樣:
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>
類型是分布式 object 類型,因為它是我們立即索引到的映射類型。 當K
為"a"
時,您將獲得ModuleA
類型,而當K
為"b"
時,您將獲得ModuleB
類型。 而且,重要的是, Module
類型本身就是一個與原始ModuleA | ModuleB
完全對應的聯合。 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()
被視為單個通用 function 類型<"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
}
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.