Reputation: 10298
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
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
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.
interface MyClassEvents {
'add': (el: string, wasNew: boolean) => void;
'delete': (changedCount: number) => void;
}
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;
}
EventEmitter
:class MyClass extends EventEmitter {
constructor() {
super();
}
}
Now you will get type checking for on
and emit
functions:
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
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
Upvotes: 10
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
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
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
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