Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
jgornick opened this issue Nov 24, 2019 · 6 comments · Fixed by #5
Labels
bug Something isn't working help wanted Extra attention is needed

Comments

@jgornick
Copy link

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 andywer added bug Something isn't working help wanted Extra attention is needed labels Nov 24, 2019
@andywer
Copy link
Owner

andywer commented Nov 24, 2019

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
Copy link
Author

jgornick commented Jan 2, 2020

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
Copy link

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
Copy link
Owner

andywer commented May 19, 2020

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

Will check it out.

@andywer
Copy link
Owner

andywer commented May 19, 2020

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:

  • Simple
  • Tested it, works
  • Your IDE can still suggest values to you when you use the method

Con:

  • We lose a bit of type safety as you can now compare .eventNames() to random strings and TS will not complain if you compare it to a string that's not an event of the emitter

andywer added a commit that referenced this issue May 19, 2020
Sacrifice a little bit of type safety in order to be able to do `class MyEmitter extends EventEmitter implements TypedEventEmitter<MyEvents>`.

Fixes #3.
andywer added a commit that referenced this issue May 23, 2020
Sacrifice a little bit of type safety in order to be able to do `class MyEmitter extends EventEmitter implements TypedEventEmitter<MyEvents>`.

Fixes #3.
@andywer
Copy link
Owner

andywer commented May 24, 2020

Fix published as v1.1.0 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working help wanted Extra attention is needed
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants