eventemitter3
eventemitter3 copied to clipboard
Cannot extend types on inheriting classes
I have a class that extends EventEmitter. I have added type support on that event emitter and everything works pretty well:
// BaseClass.ts
import EventEmitter from "eventemitter3";
type EventTypes = {
foo: () => void;
bar: (error: Error) => void;
};
export default class extends EventEmitter<EventTypes> {
constructor() {
super();
this.emit("foo");
this.emit("bar", new Error("bar"));
}
}
Problems start when I try to extend that class, and the child class needs to emit some extra events. To support that type of functionality I had to add generics on my previous class
// BaseClass.ts
import EventEmitter from "eventemitter3";
type EventTypes = {
foo: () => void;
bar: (error: Error) => void;
};
export default class<T extends object> extends EventEmitter<EventTypes & T> {
constructor() {
super();
this.emit("foo");
this.emit("bar", new Error("bar"));
}
}
and my ChildClass:
// ChildClass.ts
import BaseClass from "./BaseClass";
type ExtraTypes = {
baz: () => void;
};
export default class extends BaseClass<ExtraTypes> {
constructor() {
super();
this.emit("foo");
this.emit("bar", new Error("bar"));
this.emit("baz");
}
}
While ChildClass is OK, TS compiler starts complaining on BaseClass on every emit generating the following error:
Argument of type '"foo"' is not assignable to parameter of type 'EventNames<EventTypes & T>'
I am not sure if this is a weakness in TS, eventemitter3's type system or something wrong in my code but how can I bypass the problem?
Codesanbox link: https://codesandbox.io/s/pedantic-drake-d73tv
Hey @antoniom, I’m having the same issue. Did you find a solution?
@alexryd I actually ended up implementing my own on* callbacks and using different instances of EventEmitter on each class. It turned out that I never had to emit a parent event from my child class. Something like this:
// BaseClass.ts
import EventEmitter from "eventemitter3";
type EventTypes = {
foo: () => void;
bar: (error: Error) => void;
};
export default class {
private _ee: EventEmitter<EventTypes> = new EventEmitter();
constructor() {
super();
this.emit("foo");
this.emit("bar", new Error("bar"));
}
public onFoo = (func: EventTypes["foo"]) => this._ee.on("foo", func)
public onBar = (func: EventTypes["foo"]) => this._ee.on("bar", func)
}
// ChildClass.ts
import BaseClass from "./BaseClass";
type ExtraTypes = {
baz: () => void;
};
export default class extends BaseClass {
private _ee: EventEmitter<ExtraTypes> = new EventEmitter();
constructor() {
super();
}
}
I'm in exactly the same position. Not sure if it's a limitation of TypeScript or of this library.
Same here, still trying some workarounds
I wish I had discovered this issue a while ago, but then I may not have discovered my final (for-now) resolution.
The 2 remaining issues I have:
- Emitting from within the class that extends EventEmitter<MyTypes>
- An interface extending EventEmitter<MyTypes> does not properly restrict to MyTypes event names
Both issues are detailed here along with my final working solution.
Hopefully it helps!
Cheers!
From stackoverflow less code comments:
type TRaceEvents = {
raceUpdate: (race: IRace | null) => void;
}
interface IRaceManager extends EventEmitter {
race: IRace;
start() => void;
}
class StandardRaceManager extends EventEmitter<TRaceEvents> implements IRaceManager {
constructor(public race: IRace) { super(); }
start() {
this.race.status = "Running";
this.emit("raceUpdate", this.race as any); // `as any` fixes squiggly but isn't needed for compile or runtime
}
let race = {_id: 1, status: "taxiing"} as IRace;
const standardRace: IRaceManager = new StandardRaceManager(race);
standardRace.on("raceUpdate", race => console.log(race));
standardRace.start();
standardRace.emit("raceUpdate", standardRace.race);
// Emits are restricted to TRaceEvents
race = {_id: 2, status: "taxiing"} as IRace;
class AnotherRaceManager extends StandardRaceManager {}
const anotherRace: IRaceManager = new AnotherRaceManager(race);
anotherRace.start();
anotherRace.emit("raceUpdate", anotherRace.race); // `as any` is not required here, for some reason.
Anyone found a solution to this?
This worked for me:
// BaseClass.ts
import EventEmitter from "eventemitter3";
export interface EventTypes {
foo: () => void;
bar: (error: Error) => void;
}
export class BaseClass<T extends EventTypes> extends EventEmitter<EventTypes | T> {
constructor() {
super();
this.emit("foo");
this.emit("bar", new Error("bar"));
}
}
// ChildClass.ts
import { BaseClass, EventTypes } from "./BaseClass";
interface ExtraTypes extends EventTypes {
baz: () => void;
}
export class ChildClass extends BaseClass<ExtraTypes> {
constructor() {
super();
this.emit("foo");
this.emit("bar", new Error("bar"));
this.emit("baz");
}
}
Summary of changes:
- I changed the types to interfaces so that I could extend from one another
-
T extends EventType
instead ofT extends object
so that I could useEventType | T
, instead of&
. This works sinceT
extendsEventType
, but it feels a bit hackish.
Confirming that @pauloddr 's solution works perfectly.
I changed the types to interfaces so that I could extend from one another
You can also do this with types, using the &
operator to extend:
import EventEmitter from "eventemitter3";
export class BaseAdapter<T extends BaseEvents> extends EventEmitter<BaseEvents | T> {
constructor() {
super()
this.emit('base-event', { foo: 'bar' })
}
}
export class CustomAdapter extends BaseAdapter<CustomEvents> {
constructor() {
super()
this.emit('base-event', { foo: 'bar' })
this.emit('custom-event', { baz: 42 })
}
}
type BaseEvents = {
'base-event': (payload: { foo: string }) => void
}
type CustomEvents = BaseEvents & {
'custom-event': (payload: { baz: number }) => void
}`
This worked for me:
// BaseClass.ts import EventEmitter from "eventemitter3"; export interface EventTypes { foo: () => void; bar: (error: Error) => void; } export class BaseClass<T extends EventTypes> extends EventEmitter<EventTypes | T> { constructor() { super(); this.emit("foo"); this.emit("bar", new Error("bar")); } }
// ChildClass.ts import { BaseClass, EventTypes } from "./BaseClass"; interface ExtraTypes extends EventTypes { baz: () => void; } export class ChildClass extends BaseClass<ExtraTypes> { constructor() { super(); this.emit("foo"); this.emit("bar", new Error("bar")); this.emit("baz"); } }
Summary of changes:
- I changed the types to interfaces so that I could extend from one another
T extends EventType
instead ofT extends object
so that I could useEventType | T
, instead of&
. This works sinceT
extendsEventType
, but it feels a bit hackish.
This solution works.! But further, how to implement multi-level inheritance. Does anyone have an idea?
For now I have to make every child class at each level a generic class.