【问题标题】:Type inference of overloaded function重载函数的类型推断
【发布时间】:2020-12-08 05:07:00
【问题描述】:

我有以下类的一个实例:

// Interface from library
interface EventEmitter {
  on(eventName: 'click', handler: () => void): void
  on(eventName: 'change', handler: (newVal: string) => void): void
  // much more events
}

我想以通用方式将此事件订阅转换为 rxjs 可观察对象:

class EventHolder {
  click$ = this.fromEvent('click')
  change$ = this.fromEvent('change')

  private emitter

  constructor(instance: EventEmitter) {
    this.emitter = instance
  }

  // here we lose the type
  private fromEvent<E, T>(eventName: E): Observable<T> {
    // No overload matches this call.
    return new Observable<T>(o => this.emitter.on(eventName, o.next))
  }
}

但这不起作用,因为 typescript 无法推断 E 的类型。 有没有一种很好的方法来编写这个fromEvent 函数并保持类型安全?

目前我执行以下操作:

class EventHolder {
  click$ = this.fromClickEvent()
  change$ = this.fromChangeEvent()

  private emitter

  constructor(instance: EventEmitter) {
    this.emitter = instance
  }

  private fromClickEvent(): Observable<void> {
    return new Observable<void>(o => this.emitter.on('click', o.next))
  }

  private fromChangeEvent(): Observable<string> {
    return new Observable<string>(o => this.emitter.on('change', o.next))
  }
}

有没有更好的方法,重复代码更少?

【问题讨论】:

    标签: typescript rxjs


    【解决方案1】:

    不幸的是,overloads 并没有真正提供一种以类型安全的方式提取所需信息的方法...至少不是那种使用起来不乏味的方法(请参阅this answer 以获得疯狂的解决方案从重载函数中提取调用签名;它绝对不能扩展)。有一个未解决的问题,microsoft/TypeScript#14107 要求以某种方式将重载合并为单个调用签名,但它不是今天语言的一部分。

    如果EventEmitteron 方法是一个看起来像这样的通用方法会更好:

    type EventMap = {
        click: void;
        change: string;
        // much more events
    }
    interface BetterEventEmitter {
      on<E extends keyof EventMap>(eventName: E, handler: (newVal: EventMap[E]) => void): void;
    }
    

    在这里您可以看到EventMap 类型将eventName 映射到handler 回调预期的参数类型。同样,如果有一些原则性的方法来以编程方式操作重载,您可能可以从现有的EventEmitter 派生EventMap,如下所示:

    type EventMap = _EM<Omit<OverloadedParameters<EventEmitter['on']>, keyof any[]>>
    type _EM<T extends Record<keyof T, [string, (...args: any) => void]>> = {
      [K in keyof T as T[K][0]]: [...Parameters<T[K][1]>, void][0]
    }
    

    但由于没有可扩展的OverloadedParameters&lt;&gt; 类型函数可供使用,您不妨自己写出EventMap

    有了这个版本的EventEmitter,你可以直接写出fromEvent()

    class EventHolder {
      click$ = this.fromEvent('click')
      change$ = this.fromEvent('change')
    
      private emitter
    
      constructor(instance: BetterEventEmitter) {
        this.emitter = instance
      }
    
      private fromEvent<E extends keyof EventMap>(eventName: E) {
        return new Observable<EventMap[E]>((o: Subscriber<EventMap[E]>) =>
          this.emitter.on(eventName, o.next))
      }
    }
    

    很好,但同样,您必须手动写出EventMap


    除了询问编写具有大量重载列表的库的人将其方法更改为通用版本之外,我能想到的唯一安慰是,您至少可以检测您的 EventMap 在某些方面是否不正确:

    type UnionToIntersection<U> =
      (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
    type AlsoEventEmitter = UnionToIntersection<{ [K in keyof EventMap]: {
      on(eventName: K, handler: (newVal: EventMap[K]) => void): void
    } }[keyof EventMap]>
    type AcceptableEventEmitter<
      T extends EventEmitter = AlsoEventEmitter,
      // error here? --------> ~~~~~~~~~~~~~~~~
      U extends AlsoEventEmitter = EventEmitter
      // error here? ------------> ~~~~~~~~~~~~
      > = void;
    // errors above indicate a problem with EventMap
    

    AlsoEventEmitter 类型是重载的综合列表,您可以直接将其与 EventEmitter 进行比较。 (您不能以编程方式读取重载列表,但可以生成一个)。如果来自EventMap 的属性丢失/额外/不正确,您应该至少得到一个错误,上面告诉您问题。

    Playground link to code

    【讨论】:

    • 很酷的解决方案!当有两个或多个参数的事件时我们可以使用它吗?例如。 on(eventName: 'click', handler: (x: number, y: number) =&gt; void): void。只是一个例子。我会要求作者每个事件只使用一个 event-object-param 并切换到您的通用解决方案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-09
    • 2023-03-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多