Orion
Orion

Reputation: 57

Typescript adding types to EventEmitter

I am trying to make my code more readable and concise.

The biggest problem I am currently having is with the EventEmitter class. Every time I create a new class that uses EventEmitter I have to declare all the functions of the EventEmitter to make the class easier to understand and use.

Ex:

interface Hello {
    addListener(event: 'hi', listener: (message: string) => void): this;
    on(event: 'hi', listener: (message: string) => void): this;
    ...
}

class Hello extends EventEmitter {
    constructor() { super(); }
}

I have searched around for a solution but I couldn't find anything that suits me so I tryed to come up with my own.

interface EventEmitterEvents {
    [event: string]: any[];
}

interface EventEmitterType<T extends EventEmitterEvents> extends EventEmitter {
    addListener<K extends keyof T>(event: K, listener: (...args: T[K]) => void): this;
    on<K extends keyof T>(event: K, listener: (...args: T[K]) => void): this;
    ...
}

class Hello extends EventEmitter implements EventEmitterType<{'hi': [string]}> { ... }

But when I try to implement the interface EventEmitterType it throws an error

types of property 'addListener' are incompatible

I have figured out that for some reason in the 'addListener' and functions alike, type event is said to be 'string' | 'symbol' | 'number' which is incompatible with the EventEmitter where it is 'string' | 'symbol' but in the EventEmitterEvents I have defined that event is of type 'string'.

Question: Is there any way to fix this, and if not, is there any other way of recreating this functionality (not having to type all those functions)?

Edit: If I set event argument to 'string' it will still throw error because of listener which is also incompatible with addListener saying 'any[]' is not assignable to type 'T[K]'.

Upvotes: 1

Views: 3629

Answers (2)

bloo
bloo

Reputation: 1550

I really like the functionality that you can get from getting this sort of extension right. Anyhow, here are some ideas I would like to share that might help:

  1. Depending on you intention here, maybe not implementing the interface is a good idea, but defining an abstract class that will extend the event emitter only, like so:

interface EventEmitterEvents {
    [event: string]: any[];
}
abstract class EventEmitterType<T extends EventEmitterEvents> extends EventEmitter {
    protected constructor() {
        super();

        // do stuff here
    }
    addListener<K extends keyof T | symbol>(event: K, listener: (...args: T[Extract<string, K>]) => void) {
        // do stuff here
        return super.addListener(event, listener);
    }
    on<K extends keyof T | symbol>(event: K, listener: (...args: T[Extract<string, K>]) => void) {
        // do stuff here
        return super.on(event, listener);
    }
}
class Hello extends EventEmitterType<{ hi: [string] }> {}

  1. Bootstrapping the event emitter with the functionality that you want instead of extending the class like this.
/*
 * So the idea is to define a function that will convert the event emitter to your
 * desired design by strapping on the new stuff and ideas that you have.
 */

// So we have our interface from your sample code
interface EventEmitterEvents {
    [event: string]: any[];
}

interface EventEmitterType<T extends EventEmitterEvents> {
    addListener<K extends keyof T>(event: K, listener: (...args: T[K]) => void): this;
    on<K extends keyof T>(event: K, listener: (...args: T[K]) => void): this;
    ...
}

// then you define a function that will cast your interface onto the event emitter
function bootstrap<T extends EventEmitterEvents>(emitter: EventEmitter) {
  return emitter as EventEmitterType<T>
}
// this introduces a grey area in your code that you might not like though.

I hope this is of some use to your goal here.

Upvotes: 3

DarrenDanielDay
DarrenDanielDay

Reputation: 76

Maybe narrowing the type of event names to string like Extract<keyof T, string> could help.

Or if you just want the result of keyof operator to be a string, add configuration with "keyofStringsOnly": true in tsconfig.json.

Upvotes: 0

Related Questions