primus / eventemitter3

EventEmitter3 - Because there's also a number 2. And we're faster.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Cannot extend types on inheriting classes

antoniom opened this issue · comments

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:

  1. Emitting from within the class that extends EventEmitter<MyTypes>
  2. 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 of T extends object so that I could use EventType | T, instead of &. This works since T extends EventType, 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 of T extends object so that I could use EventType | T, instead of &. This works since T extends EventType, 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.