Recipe · Node and platforms

How do I type a Node.js EventEmitter's events in JavaScript?

  • Node.js (ES modules)
  • EventEmitter
  • @extends
  • @types/node
  • Checked with TypeScript 5.9

The problem My OrderEvents class extends EventEmitter. Listeners get any for their arguments, and a misspelled event name is never caught. Can JSDoc tell tsc which events exist and what they carry?

The short answer

Describe each event as a tuple of its arguments, and pass that map to EventEmitter with @extends:

order-events.js order-events.ts TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
import { EventEmitter } from "node:events";

/** @typedef {{ id: string, total: number }} Order */

/**
 * @typedef {object} OrderEventMap
 * @property {[order: Order]} shipped
 * @property {[id: string, reason: string]} cancelled
 */

/** @extends {EventEmitter<OrderEventMap>} */
export class OrderEvents extends EventEmitter {}

const events = new OrderEvents();
events.on("shipped", (order) => console.log(`Shipped ${order.id}: ${order.total.toFixed(2)}`));
events.on("cancelled", (id, reason) => console.log(`${id} cancelled: ${reason.trim()}`));

events.emit("shipped", { id: "A-1001", total: 40 });
events.emit("cancelled", "A-1002", "out of stock");
import { EventEmitter } from "node:events";

type Order = { id: string; total: number };

interface OrderEventMap {
  shipped: [order: Order];
  cancelled: [id: string, reason: string];
}

export class OrderEvents extends EventEmitter<OrderEventMap> {}

const events = new OrderEvents();
events.on("shipped", (order) => console.log(`Shipped ${order.id}: ${order.total.toFixed(2)}`));
events.on("cancelled", (id, reason) => console.log(`${id} cancelled: ${reason.trim()}`));

events.emit("shipped", { id: "A-1001", total: 40 });
events.emit("cancelled", "A-1002", "out of stock");
An EventEmitter typed with its events and their arguments.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": ["node"]
  },
  "include": ["*.js"]
}

The listener for "shipped" receives an Order, the one for "cancelled" an id and a reason, and every emit is checked against the same map.

Why it works

@types/node declares EventEmitter as a generic class whose type argument maps event names to argument tuples. on, once, off and emit read their parameter types from it. In JavaScript the type argument of a base class goes in an @extends tag, the same as for any generic base class. The tuple labels (order:, reason:) only name the arguments in hovers.

The same thing in TypeScript

The TypeScript equivalent tab writes class OrderEvents extends EventEmitter<OrderEventMap> {} with an interface for the map. The class body and the calls are the same.

What goes wrong

A payload of the wrong type is TS2345, a missing argument is TS2554, and an event that is not in the map fails at the listener, whose type becomes never:

order-events.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: order-events.js(10,24): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Order'. order-events.js(12,8): error TS2554: Expected 3 arguments, but got 2. order-events.js(14,21): error TS2345: Argument of type '() => void' is not assignable to parameter of type 'never'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. order-events.js(10,24): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Order'. order-events.js(12,8): error TS2554: Expected 3 arguments, but got 2. order-events.js(14,21): error TS2345: Argument of type '() => void' is not assignable to parameter of type 'never'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. order-events.js(10,24): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Order'. order-events.js(12,8): error TS2554: Expected 3 arguments, but got 2. order-events.js(14,21): error TS2345: Argument of type '() => void' is not assignable to parameter of type 'never'. checked 21 October 2026
import { EventEmitter } from "node:events";

/** @typedef {{ id: string, total: number }} Order */

/** @extends {EventEmitter<{ shipped: [order: Order], cancelled: [id: string, reason: string] }>} */
export class OrderEvents extends EventEmitter {}

const events = new OrderEvents();
events.emit("shipped", "A-1001");error TS2345: Argument of type 'string' is not assignable to parameter of type 'Order'.
events.emit("cancelled", "A-1002");error TS2554: Expected 3 arguments, but got 2.
events.on("shiped", () => console.log("never called"));error TS2345: Argument of type '() => void' is not assignable to parameter of type 'never'.
A wrong payload, a missing argument, and an event the map does not have.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": ["node"]
  },
  "include": ["*.js"]
}

Without the map, nothing is checked: the listener’s argument is any and a misspelled event name is accepted:

order-events.js TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
import { EventEmitter } from "node:events";

const events = new EventEmitter();
events.on("shipped", (order) => console.log(order.totl));
events.emit("shiped", "not an order");
A plain EventEmitter accepts anything.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "types": ["node"]
  },
  "include": ["*.js"]
}

Gotchas

  • The never message means “unknown event”. Argument of type '() => void' is not assignable to parameter of type 'never' on an on() call says the event name is not in the map.
  • error events are special at runtime. Node.js throws when an "error" event has no listener; add error: [error: Error] to the map and a listener if your emitter emits one.
  • Typed events stop at the class. Code that receives the emitter as a plain EventEmitter sees untyped events again; pass it as OrderEvents.

Questions

Which version of @types/node supports the event map?

The @types/node 24 used on this page declares EventEmitter with a type parameter for the map. Older releases of @types/node declared it without one; if yours does, update the package to match your Node.js version.

Does this work for an emitter I create without a class?

Yes: /** @type {EventEmitter<OrderEventMap>} */ const events = new EventEmitter(); types a plain instance the same way.

Sources

Last reviewed 21 October 2026.