简体   繁体   中英

Type inference of overloaded function

I have an instance of the following class:

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

I want to convert this event subscription to rxjs observables in a generic way:

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))
  }
}

But this does not work, because typescript can not infer the type of E. Is there a nice way to write this fromEvent function and keep the type safety?

Currently I do the following:

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))
  }
}

Is there a nicer way with less repeating code?

Unfortunately, overloads don't really provide a way to extract the information you want in a type safe way... at least not one that isn't tedious to use (see this answer for a crazy solution to pull out the call signatures from an overloaded function; it definitely doesn't scale). There is an open issue, at microsoft/TypeScript#14107 asking for some way to collapse overloads into a single call signature, but it's not part of the language today.

It would have been better for EventEmitter 's on method to be a generic method looking like this:

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

Here you see that the EventMap type maps eventName to the type of the argument expected by the handler callback. Again, if there were some principled way to manipulate overloads programmatically, you could probably derive EventMap from the existing EventEmitter like this:

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]
}

but since there's no scalable OverloadedParameters<> type function to use, you might as well just write out EventMap by yourself.

Armed with this version of EventEmitter , you could write the fromEvent() in a straightforward way:

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))
  }
}

Great, but again, you have to write out EventMap by hand.


The only consolation I can think of, other than asking whoever wrote the library with the huge list of overloads to change their method to the generic version, is that you can at least detect if your EventMap is incorrect in some way:

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

The AlsoEventEmitter type is a synthesized list of overloads, which you can compare directly against EventEmitter . (You can't programmatically read a list of overloads but you can produce one). If a property from EventMap is missing/extra/incorrect, you should get at least one error above telling you the issue.

Playground link to code

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM