Typesafe listeners using EventEmitter and Typescript in Node.js

Viewed 3643

Given a Typescript interface and a class extending Node.js EventEmitter, is it possible to define custom listeners that give typesafe check for the function arguments?

Given the following example:

import { EventEmitter } from 'events';

interface Payload {
    id: string;
    weight: number;
}

class CustomEventEmitter extends EventEmitter {
    constructor() {
        super();

        this.on('my_event', (data) => {
            // I would like data to be implicitly inferred as Payload type
            console.log(data.weight); // This should compile
            console.log(data.something); // This should not compile
        });
    }
}

A node EventEmitter listener is defined as (...args: any[]) => void), I would like to override the any[] type and use custom defined types instead. Is it possible?

1 Answers

Yep, there's an awesome package called Typed-Emitter (link to project) that works really well for me.

From their docs:

import {EventEmitter} from "events" // I made a slight change here as I've needed to explicitly import EventEmitter from events
import TypedEmitter from "typed-emitter"

// Define your emitter's types like that:
// Key: Event name; Value: Listener function signature
interface MessageEvents {
  error: (error: Error) => void,
  message: (body: string, from: string) => void
}

const messageEmitter = new EventEmitter() as TypedEmitter<MessageEvents>

// Good 
messageEmitter.emit("message", "Hi there!", "no-reply@test.com")

// TypeScript will catch those mistakes ✋
messageEmitter.emit("mail", "Hi there!", "no-reply@test.com")
messageEmitter.emit("message", "Hi there!", true)

// Good 
messageEmitter.on("error", (error: Error) => { /* ... */ })

// TypeScript will catch those mistakes ✋
messageEmitter.on("error", (error: string) => { /* ... */ })
messageEmitter.on("failure", (error: Error) => { /* ... */ })
class MyEventEmitter extends (EventEmitter as new () => TypedEmitter<MyEvents>) {
  // ...
}

For your example, this should work:


import { EventEmitter } from 'events';
import TypedEmitter from "typed-emitter"

interface Payload {
    id: string;
    weight: number;
}

interface CustomEventEmitterEvents {
  my_event: (data: Payload) => void
}

class CustomEventEmitter extends (EventEmitter as new () => TypedEmitter<CustomEventEmitterEvents>) {
    constructor() {
        super();

        this.on('my_event', (data) => {
            // I would like data to be implicitly inferred as Payload type
            console.log(data.weight); // This should compile
            console.log(data.something); // This should not compile
        });
    }
}
Related