andywer / typed-emitter

🔩 Type-safe event emitter interface for TypeScript
MIT License
268 stars 24 forks source link

Extending EventEmitter and implementing TypedEmitter results in incompatible eventNames type #3

Closed jgornick closed 4 years ago

jgornick commented 4 years ago

For example:

interface FooEvents {
  bar: () => void
}

class FooEmitter extends EventEmitter implements TypedEmitter<FooEvents> {
  // ...
}

Results in error:

Class 'FooEmitter' incorrectly implements interface 'TypedEventEmitter<FooEvents>'.
  The types returned by 'eventNames()' are incompatible between these types.
    Type '(string | symbol)[]' is not assignable to type '"bar"[]'.
      Type 'string | symbol' is not assignable to type '"bar"'.
        Type 'string' is not assignable to type '"bar"'.ts(2420)

Workaround is to implement eventNames and override typing:

public eventNames (): (keyof FooEvents)[] {
  return super.eventNames() as (keyof FooEvents)[]
}
andywer commented 4 years ago

Hey @jgornick, thanks for reporting!

I have had a look at it, but unfortunately I wasn't able to find a solution yet.

If someone else has an idea... The sample code looks something like this:

import EventEmitter from "events"
import TypedEventEmitter from "typed-emitter"

interface FooEvents {
  bar(text: string): void
}

export default class FooEmitter extends EventEmitter implements TypedEventEmitter<FooEvents> {}
jgornick commented 4 years ago

To get around the issue for now, I created a delegation class that's typed properly:

import { EventEmitter as CoreEventEmitter } from 'events'
import TypedEmitter from 'typed-emitter'

type Arguments<T> = [T] extends [(...args: infer U) => any]
  ? U
  : [T] extends [void] ? [] : [T]

export class EventEmitter<Events> implements TypedEmitter<Events> {
  public eventEmitter = new CoreEventEmitter() as unknown as TypedEmitter<Events>

  public addListener<E extends keyof Events> (event: E, listener: Events[E]): this {
    this.eventEmitter.addListener(event, listener)
    return this
  }

  public on<E extends keyof Events> (event: E, listener: Events[E]): this {
    this.eventEmitter.on(event, listener)
    return this
  }

  public once<E extends keyof Events> (event: E, listener: Events[E]): this {
    this.eventEmitter.once(event, listener)
    return this
  }

  public prependListener<E extends keyof Events> (event: E, listener: Events[E]): this {
    this.eventEmitter.prependListener(event, listener)
    return this
  }

  public prependOnceListener<E extends keyof Events> (event: E, listener: Events[E]): this {
    this.eventEmitter.prependOnceListener(event, listener)
    return this
  }

  public off<E extends keyof Events>(event: E, listener: Events[E]): this {
    this.eventEmitter.off(event, listener)
    return this
  }

  public removeAllListeners<E extends keyof Events> (event: E): this {
    this.eventEmitter.removeAllListeners(event)
    return this
  }

  public removeListener<E extends keyof Events> (event: E, listener: Events[E]): this {
    this.eventEmitter.removeListener(event, listener)
    return this
  }

  public emit<E extends keyof Events> (event: E, ...args: Arguments<Events[E]>): boolean {
    return this.eventEmitter.emit(event, ...args)
  }

  public eventNames (): (keyof Events)[] {
    return this.eventEmitter.eventNames() as (keyof Events)[]
  }

  public listeners<E extends keyof Events> (event: E): Function[] {
    return this.eventEmitter.listeners(event)
  }

  public listenerCount<E extends keyof Events> (event: E): number {
    return this.eventEmitter.listenerCount(event)
  }

  public getMaxListeners (): number {
    return this.eventEmitter.getMaxListeners()
  }

  public setMaxListeners (maxListeners: number): this {
    this.eventEmitter.setMaxListeners(maxListeners)
    return this
  }
}
yoursunny commented 4 years ago

The strict-event-emitter-types package solves the extending EventEmitter problem with this:

class MyEventEmitter extends (EventEmitter as { new(): MyEmitter }) {
}

Would it help here?

andywer commented 4 years ago

That looks like a pretty nifty solution. Thanks for sharing, @yoursunny!

Will check it out.

andywer commented 4 years ago

So I tried that trick, but it doesn't work for this use case that way.

I propose to solve this issue for now by changing the eventNames() signature:

- eventNames (): (keyof Events)[]
+ eventNames (): (keyof Events | string | symbol)[]

Pro:

Con:

andywer commented 4 years ago

Fix published as v1.1.0 🚀