aleung
aleung

Reputation: 10298

Declaring events in a TypeScript class which extends EventEmitter

I have a class extends EventEmitter that can emit event hello. How can I declare the on method with specific event name and listener signature?

class MyClass extends events.EventEmitter {

  emitHello(name: string): void {
    this.emit('hello', name);
  }

  // compile error on below line
  on(event: 'hello', listener: (name: string) => void): this;
}

Upvotes: 58

Views: 72951

Answers (7)

Morglod
Morglod

Reputation: 217

You can use my typed event emitter package for this.

eg:

import { EventEmitter } from 'tsee';

const events = new EventEmitter<{
    foo: (a: number, b: string) => void,
}>();

// foo's arguments is fully type checked
events.emit('foo', 123, 'hello world');

This package also provide interfaces & some utils.

Upvotes: 6

Binier
Binier

Reputation: 1107

to extend @SergeyK's answer, with this you can get type-checking and completion on both emit and on functions without repeating event types.

  1. Define event listener signatures for each event type:
interface MyClassEvents {
  'add': (el: string, wasNew: boolean) => void;
  'delete': (changedCount: number) => void;
}
  1. Declare interface which constructs types for MyClass, based on EventListeners (MyClassEvents) function signature:
declare interface MyClass {
  on<U extends keyof MyClassEvents>(
    event: U, listener: MyClassEvents[U]
  ): this;

  emit<U extends keyof MyClassEvents>(
    event: U, ...args: Parameters<MyClassEvents[U]>
  ): boolean;
}
  1. Simply define you class extending EventEmitter:
class MyClass extends EventEmitter {
  constructor() {
    super();
  }
}

Now you will get type checking for on and emit functions:

enter image description here

enter image description here

Unfortunately you will get completion and type-checking only on those two functions (unless you define more functions inside MyClass interface).

To get more generic solution, you can use this package I wrote. note: it adds no runtime overhead.

import { TypedEmitter } from 'tiny-typed-emitter';

interface MyClassEvents {
  'add': (el: string, wasNew: boolean) => void;
  'delete': (changedCount: number) => void;
}

class MyClass extends TypedEmitter<MyClassEvents> {
  constructor() {
    super();
  }
}

Upvotes: 73

Meirion Hughes
Meirion Hughes

Reputation: 26398

I'm not sure as of when, but this is available in the node typings now. You just have to pass a Record<string, any[]> (event-key > arguments tuple) type into the generic of EventEmitter<T>

import { EventEmitter } from 'events';

interface MyClassEvents {
  "hello": [name: string];
}

class MyClass extends EventEmitter<MyClassEvents> { }

const foo = new MyClass();

this will type the EventEmitter's methods to have these events typed for the consumer

IDE auto complete of event name

IDE showing event arg field-name and field-type infered

Upvotes: 10

Vasp
Vasp

Reputation: 69

Something to add is, often with event emitters you'll want to conjoin a string and an enum:

type EventType<U extends string> = `${string}::${U}`;

declare interface SocketClients {
  on: <U extends keyof SocketEvents>(event: EventType<U>, listener: (args: SocketEvents[U]) => void) => this;
  emit: <U extends keyof SocketEvents>(event: EventType<U>, args: SocketEvents[U]) => boolean;
}

This will let you scope listeners, and retain typings.

Upvotes: 0

Brian McBarron
Brian McBarron

Reputation: 61

I really liked @Binier's answer and especially the generic solution offered by tiny-typed-emitter. As an alternative, I wrote up this pure-typescript version:

type EmittedEvents = Record<string | symbol, (...args: any) => any>;

export declare interface TypedEventEmitter<Events extends EmittedEvents> {
  on<E extends keyof Events>(
    event: E, listener: Events[E]
  ): this;

  emit<E extends keyof Events>(
    event: E, ...args: Parameters<Events[E]>
  ): boolean;
}

export class TypedEventEmitter<Events extends EmittedEvents> extends EventEmitter {}

It's used similarly:

type MessageSocketEvents = {
  'message': (json: object) => void;
  'close': () => void;
};

export class MessageSocket extends TypedEventEmitter<MessageSocketEvents> {
  ...
}

Upvotes: 5

SergeyK
SergeyK

Reputation: 1577

Most usable way of doing this, is to use declare:

declare interface MyClass {
    on(event: 'hello', listener: (name: string) => void): this;
    on(event: string, listener: Function): this;
}

class MyClass extends events.EventEmitter {
    emitHello(name: string): void {
        this.emit('hello', name);
    }
}

Note that if you are exporting your class, both the interface and class have to be declared with the export keyword.

Upvotes: 111

Isaac Nassimi
Isaac Nassimi

Reputation: 121

Here's what I was able to figure out. Overriding the default function with a generic!

interface IEmissions {
  connect: () => void
  test: (property: string) => void
}

class MyClass extends events.EventEmitter {
  private _untypedOn = this.on
  private _untypedEmit = this.emit
  public on = <K extends keyof IEmissions>(event: K, listener: IEmissions[K]): this => this._untypedOn(event, listener)
  public emit = <K extends keyof IEmissions>(event: K, ...args: Parameters<IEmissions[K]>): boolean => this._untypedEmit(event, ...args)

  this.emit('test', 'Testing') // This will be typed for you!
}

// Example:
const inst = new MyClass()
inst.on('test', info => console.log(info)) // This will be typed!

Upvotes: 12

Related Questions