andywer / typed-emitter

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

Generics example for 2.x does not compile #32

Open sp00x opened 2 years ago

sp00x commented 2 years ago

The example for generics in the documentation does not work:

class MyEventEmitter<T> extends (EventEmitter as { new<T>(): TypedEmitter<T> })<T> {
  // ...
}

Just gives error TS2344: Type 'T' does not satisfy the constraint 'EventMap'. on the TypedEmitter<T> part...

TypeScript 4.5.5

sp00x commented 2 years ago

Also tried adding it as a constraint on T, but same happens with class MyEventEmitter<T extends EventMap> extends (EventEmitter as { new<T>(): TypedEmitter<T> })<T> { .. } - still the same error

andywer commented 2 years ago

Also tried adding it as a constraint on T, but same happens with class MyEventEmitter extends (EventEmitter as { new(): TypedEmitter }) { .. } - still the same error

That should work, though. Can you provide a minimal sample? @sp00x

sp00x commented 2 years ago

well...

Basically just that one line, even if it doesn't make much sense:

import TypedEmitter from 'typed-emitter';
import EventEmitter from 'events';

class MyEventEmitter<T> extends (EventEmitter as { new<T>(): TypedEmitter<T> })<T> {
  // whatever
}

or if you want to use it with your example MessageEvents event type

import TypedEmitter from 'typed-emitter';
import EventEmitter from 'events';

type MessageEvents = {
  error: (error: Error) => void,
  message: (body: string, from: string) => void
}

class MyEventEmitter<T> extends (EventEmitter as { new<T>(): TypedEmitter<T> })<T> {
  // whatever
}

class SomethingThatEmitsMessages extends MyEventEmitter<MessageEvents> {
  // whatever
}

You get an error on the <T>

andywer commented 2 years ago

Now I see. Yeah, that won't work… We need to constrain the generic type parameter T:

import TypedEmitter from 'typed-emitter';
+ import { EventMap } from 'typed-emitter';
import EventEmitter from 'events';

- class MyEventEmitter<T> extends (EventEmitter as { new<T>(): TypedEmitter<T> })<T> {
+ class MyEventEmitter<T extends EventMap> extends (EventEmitter as { new<T>(): TypedEmitter<T> })<T> {
  // whatever
}

Try and see if it works for you, then let's update the readme :)

sp00x commented 2 years ago

Is that not the same as I mentioned in the 2nd comment above, or am I missing something? https://github.com/andywer/typed-emitter/issues/32#issuecomment-1041313893

(If so it gives the same error..)

andywer commented 2 years ago

@sp00x Right… But how can it still say error TS2344: Type 'T' does not satisfy the constraint 'EventMap'. is there is the type constraint class MyEventEmitter<T extends EventMap>?

I think this requires a runnable code sample in TS playground / code sandbox or similar.

devjayantmalik commented 2 years ago

Hello Developers,

I faced similar problem, and here is the workaround that I found:


export class BaseEmitter<T extends EventMap> extends (EventEmitter as {
  new <T>(): TypedEmitter<T & EventMap>;
})<T> {}

Now you can use it like this:

type IAuthCustomerEvents = {
  signup: (email: string) => void;
  password_reset: (email: string) => void;
};

class AuthCustomerEmitter extends BaseEmitter<IAuthCustomerEvents> {}
sp00x commented 2 years ago

@devjayantmalik Nice, then it is actually happy! (In fact, it seems you don't need the <T extends EventMap> but only the <T & EventMap> part for TS to be happy)

https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAFQJ5gKYBMCiJgxqqAGjgG85MA3VAOxgFkBDMOAXzgDMoIQ4ByGFBgC0qHHgK8A3AFgAUKEixyVWtlz4oHLj16oVMAM5S5cgWjh1UBgwwDmqSjUNwAvKTlw4BLlABccAApvaH9MKB8ASlcAPjgKCGB0Qg84ECsbe38AgCMIdCR-AxgoYGpbYk5uQuLS2yiXWPjEuRYTWQBjABsGawskR1UxDQAeBFjUAA98anQDQIGYNXFNHtI4alQAd1HogIj-ZDQsIYJRuAAyZSdGMFiWCJ33WU8Aehe4TYALBnwqKBa2l0enMAMrcVAwT61BDfRZiAyWax2KxeKY0WZ9BZLEaIjIOfQGWIkFJvD6wvQEFpAA

sertonix commented 2 years ago

There is another way with a constrained type of T:

class MyEventEmitter<T extends EventMap> extends (EventEmitter as { new<T extends EventMap>(): TypedEmitter<T> })<T> {
  /*                ^                                                   ^                                   ^     ^
                defining first T                              defining second T                  using second T  using first T
   */
}

The reason is that T is redefined without constrain.

bbaldino commented 1 year ago

Has anyone gotten this to work with potentially multiple sub-classes in a chain? I want to be able to define a class like:

import { EventEmitter } from 'events';
import TypedEmitter, { EventMap } from 'typed-emitter';

export class BaseEmitter<T extends EventMap> extends (EventEmitter as {
  new <T>(): TypedEmitter<T & EventMap>;
})<T> {}

type ParentEvents = {
    event_one: (arg0: number) => void;
}

// A subclass can extend Parent/ParentEvents to add its own events
class Parent<T extends ParentEvents> extends BaseEmitter<T> {
    something() {
        this.emit('event_one', 42);
    }
}

but I get this error for the line trying to emit the event:

Argument of type '[number]' is not assignable to parameter of type 'Parameters<(T & EventMap)["event_one"]>'

Playground here

sertonix commented 1 year ago

I think the problem is that this is possible:

class SubParent extends Parent<{event_one: () => void}> {
    something() {
        this.emit('event_one', 42); // then this code is invalid
    }
}

This looks like a bug but I am not sure. I stopped using typescript because it took more time to get the types right than debugging a typo.

bbaldino commented 1 year ago

@Sertonix you're saying because, in that scenario, which event_one is ambiguous? I was wondering if it was related to some ambiguity as well (i.e. since it's some generic T extending, it doesn't know which)--so I was hoping casting/qualifying the call might help, but I was unable to find the right syntax to get that to work.

Xiaoyang-Huang commented 7 months ago

I found this is work for me:

import EventEmitter from "events";
import TypedEmitter, { EventMap } from "typed-emitter";

class Animal<T1 extends EventMap = {}> extends (EventEmitter as { new <T2 extends EventMap>(): TypedEmitter<T2> & TypedEmitter<{ spawn: (name: string) => void }> })<T1> {
  constructor() {
    super();
    this.emit("spawn", "abc");
  }
}

class Frog extends Animal<{ jump: (times: number) => void }> {
  constructor() {
    super();
    this.emit("spawn", "bcd");
    this.emit("jump", 3);
  }
}

class Bird<E extends EventMap = { test: () => void }> extends Animal<{ fly: () => void } & E> {}

const frog = new Frog();
const bird = new Bird();

const animals: Animal[] = [frog, bird];
animals[0].addListener("spawn", (name) => {});
frog.addListener("jump", (times) => {});
bird.addListener("test", () => {});