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

Cannot extend types on inheriting classes #243

Open
antoniom opened this issue Dec 18, 2021 · 9 comments
Open

Cannot extend types on inheriting classes #243

antoniom opened this issue Dec 18, 2021 · 9 comments

Comments

@antoniom
Copy link

antoniom commented Dec 18, 2021

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

@alexryd
Copy link

alexryd commented Feb 5, 2022

Hey @antoniom, I’m having the same issue. Did you find a solution?

@antoniom
Copy link
Author

@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();
  }
}

@kebian
Copy link

kebian commented Dec 2, 2022

I'm in exactly the same position. Not sure if it's a limitation of TypeScript or of this library.

@edolix
Copy link

edolix commented Feb 1, 2023

Same here, still trying some workarounds

@karts-with-freaking-lasers
Copy link

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.

@sharathprabhal
Copy link

Anyone found a solution to this?

@pauldps
Copy link

pauldps commented Aug 17, 2023

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.

@HerbCaudill
Copy link

HerbCaudill commented Nov 1, 2023

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
}`

@AkiraChinUp
Copy link

AkiraChinUp commented Dec 22, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants