我有一个工厂函数,它创建一个经典的on()
事件侦听器,但它特定于我希望允许用户侦听的任何类型。事件被定义为类型,它们有eventName
和data
(这是事件发出时返回的数据)。我希望保持这两个事件的相互关联,这样,如果我要侦听特定事件,则相关数据将在处理程序中可用。
考虑以下代码:
interface DefaultEventBody {
eventName: string
data: unknown
}
interface ChristmasEvent extends DefaultEventBody {
eventName: 'christmas',
data: {
numberOfPresentsGiven: number
}
}
interface EasterEvent extends DefaultEventBody {
eventName: 'easter',
data: {
numberOfEggsGiven: number
}
}
export function Listener <EventBody extends DefaultEventBody> () {
return (eventName: EventBody['eventName'], fn: (data: EventBody['data']) => void) => {
// someEmitter.on(eventName, fn)
}
}
const spoiltBrat = { on: Listener<EasterEvent|ChristmasEvent>() }
spoiltBrat.on('christmas', (data) => {
console.log(data.numberOfPresentsGiven)
})
类型脚本正确地知道eventName
I传递可以是christmas|easter
,但它无法推断处理程序上的data
类型,因此在我尝试访问data.numberOfPresentsGiven
时会出错。
我知道为什么会发生这种情况(因为ChristmasEvent
和EasterEvent
类型都不包含相同的numberOf*
属性),但我想知道是否有解决方案来实现我想要实现的目标?
根据约萨里安船长的要求,这里有一个Playground Link,其中几乎完成了脚本。
快速修复
请参阅answer:
type UnionKeys<T> = T extends T ? keyof T : never;
type StrictUnionHelper<T, TAll> =
T extends any
? T & Partial<Record<Exclude<UnionKeys<TAll>, keyof T>, never>> : never;
// credits goes to https://stackoverflow.com/questions/65805600/type-union-not-checking-for-excess-properties#answer-65805753
type StrictUnion<T> = StrictUnionHelper<T, T>
export function Listener<EventBody extends DefaultEventBody>() {
return (eventName: EventBody['eventName'], fn: (data: StrictUnion<EventBody['data']> /** <---- change is here */) => void) => {
// someEmitter.on(eventName, fn)
}
}
const spoiltBrat = {
on: Listener<EasterEvent | ChristmasEvent>()
}
spoiltBrat.on('christmas', (data) => {
data.numberOfPresentsGiven // <---- DRAWBACK, number | undefined
})
上述解决方案有效,但有其自身的缺陷。正如您可能已经注意到的,numberOfPresentsGiven
是允许的,但它可能是undefined
。这不是我们想要的。
更长的修复
通常,如果您想输入发布/订阅逻辑,您应该使用重载。 考虑这个例子:
type AllowedEvents = ChristmasEvent | EasterEvent
type Events = AllowedEvents['eventName']
// type Overloadings = {
// christmas: (eventName: "christmas", fn: (data: ChristmasEvent) => void) => void;
// easter: (eventName: "easter", fn: (data: EasterEvent) => void) => void;
// }
type Overloadings = {
[Prop in Events]: (eventName: Prop, fn: (data: Extract<AllowedEvents, { eventName: Prop }>) => void) => void
}
现在我们有了一个数据结构,其中包含on
函数的适当类型。为了将此DS应用于on
并使其充当重载,我们需要获取Overloadings
道具的联合类型并合并它们(交集)。为什么是十字路口?因为函数类型的交集会产生重载。让我们获得一个值的并集:
type Values<T>=T[keyof T]
type Union =
| ((eventName: "christmas", fn: (data: ChristmasEvent) => void) => void)
| ((eventName: "easter", fn: (data: EasterEvent) => void) => void)
type Union = Values<Overloadings>
现在,当我们有一个并集时,我们可以借助实用程序类型将其转换为交集:
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
type EventsOverload = UnionToIntersection<Union>
临时解决方案:
type AllowedEvents = ChristmasEvent | EasterEvent
type Events = AllowedEvents['eventName']
type Overloadings = {
[Prop in Events]: (eventName: Prop, fn: (data: Extract<AllowedEvents, { eventName: Prop }>['data']) => void) => void
}
type Values<T> = T[keyof T]
type Union = Values<Overloadings>
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
type EventsOverload = UnionToIntersection<Union> & ((eventName: string, fn: (data: any) => void) => void)
export function Listener(): EventsOverload {
return (eventName, fn: (data: any) => void) => {
// someEmitter.on(eventName, fn)
}
}
const spoiltBrat = {
on: Listener()
}
spoiltBrat.on('christmas', (data) => {
data.numberOfPresentsGiven // number
})
spoiltBrat.on('easter', (data) => {
data.numberOfEggsGiven // number
})
然而,它还不是完美的。您可能已经注意到我使用了any
。没有人喜欢any
。您可以提供所有允许data
参数的交集,而不是any
:
export function Listener(): EventsOverload {
return (eventName, fn: (data: ChristmasEvent['data'] & EasterEvent['data']) => void) => {
}
}
为什么要交叉?因为这是处理任何eventName
的唯一安全方法。Here您可以找到更多上下文和说明。
整体解决方案:
interface DefaultEventBody {
eventName: string
data: unknown
}
interface ChristmasEvent extends DefaultEventBody {
eventName: 'christmas',
data: {
numberOfPresentsGiven: number
}
}
interface EasterEvent extends DefaultEventBody {
eventName: 'easter',
data: {
numberOfEggsGiven: number
}
}
type AllowedEvents = ChristmasEvent | EasterEvent
type Events = AllowedEvents['eventName']
type Overloadings = {
[Prop in Events]: (eventName: Prop, fn: (data: Extract<AllowedEvents, { eventName: Prop }>['data']) => void) => void
}
type Values<T> = T[keyof T]
type Union = Values<Overloadings>
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
type EventsOverload = UnionToIntersection<Union>
export function Listener(): EventsOverload {
return (eventName, fn: (data: UnionToIntersection<AllowedEvents['data']>) => void) => { }
}
const spoiltBrat = {
on: Listener()
}
spoiltBrat.on('christmas', (data) => {
data.numberOfPresentsGiven // number
})
spoiltBrat.on('easter', (data) => {
data.numberOfEggsGiven // number
})
Playground
这里有另一个示例,取自我的blog:
const enum Events {
foo = "foo",
bar = "bar",
baz = "baz",
}
/**
* Single sourse of true
*/
interface EventMap {
[Events.foo]: { foo: number };
[Events.bar]: { bar: string };
[Events.baz]: { baz: string[] };
}
type EmitRecord = {
[P in keyof EventMap]: (name: P, data: EventMap[P]) => void;
};
type ListenRecord = {
[P in keyof EventMap]: (
name: P,
callback: (arg: EventMap[P]) => void
) => void;
};
type Values<T> = T[keyof T];
// credits goes to https://stackoverflow.com/a/50375286
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
type MakeOverloadings<T> = UnionToIntersection<Values<T>>;
type Emit = MakeOverloadings<EmitRecord>;
type Listen = MakeOverloadings<ListenRecord>;
const emit: Emit = <T,>(name: string, data: T) => { };
emit(Events.bar, { bar: "1" });
emit(Events.baz, { baz: ["1"] });
emit("unimplemented", { foo: 2 }); // expected error
const listen: Listen = (name: string, callback: (arg: any) => void) => { };
listen(Events.baz, (arg /* {baz: string[] } */) => { });
listen(Events.bar, (arg /* {bar: string } */) => { });
Playground
请记住,您的发射器和侦听器应该只有一个True源。我的意思是他们应该使用共享事件映射。
更新
在全局范围内定义类型是一种很好的做法。您几乎从不需要在函数内部声明类型。
/*
* ListenerFactory.ts
*/
interface DefaultEventBody {
eventName: string
data: unknown
}
type Values<T> = T[keyof T]
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never;
type Overloadings<E extends DefaultEventBody> = {
[Prop in E['eventName']]: (eventName: Prop, fn: (data: Extract<E, { eventName: Prop }>['data']) => void) => void
}
export function Listener<AllowedEvents extends DefaultEventBody>(): UnionToIntersection<Values<Overloadings<AllowedEvents>>>
export function Listener<AllowedEvents extends DefaultEventBody>() {
return (eventName: string, fn: (data: UnionToIntersection<AllowedEvents['data']>) => void) => { }
}
/*
* ConsumingLibrary.ts
*/
interface ChristmasEvent extends DefaultEventBody {
eventName: 'christmas',
data: {
numberOfPresentsGiven: number
}
}
interface EasterEvent extends DefaultEventBody {
eventName: 'easter',
data: {
numberOfEggsGiven: number
}
}
const spoiltBrat = {
on: Listener<ChristmasEvent | EasterEvent>()
}
spoiltBrat.on('christmas', (data) => {
data.numberOfPresentsGiven // number
})
spoiltBrat.on('easter', (data) => {
data.numberOfEggsGiven // number
})
Playground