映射数量可变的泛型,同时保留类型值之间的链接数量、类型、链接

2023-09-03 13:31:51 作者:花有解语戏天涯°

我有一个工厂函数,它创建一个经典的on()事件侦听器,但它特定于我希望允许用户侦听的任何类型。事件被定义为类型,它们有eventNamedata(这是事件发出时返回的数据)。我希望保持这两个事件的相互关联,这样,如果我要侦听特定事件,则相关数据将在处理程序中可用。

考虑以下代码:

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)

})
Python中的映射类型指的是什么

类型脚本正确地知道eventNameI传递可以是christmas|easter,但它无法推断处理程序上的data类型,因此在我尝试访问data.numberOfPresentsGiven时会出错。

属性‘number OfPresentsGiven’在类型‘{number OfPresentsGiven:Number;}|{number OfEggsGiven:Number;}’上不存在。 属性‘number OfPresentsGiven’在类型‘{number OfEggsGiven:Number;}’上不存在。(2339)

我知道为什么会发生这种情况(因为ChristmasEventEasterEvent类型都不包含相同的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