The short answer
Describe each event as a tuple of its arguments, and pass that map to EventEmitter with @extends:
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");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:
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'.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:
import { EventEmitter } from "node:events";
const events = new EventEmitter();
events.on("shipped", (order) => console.log(order.totl));
events.emit("shiped", "not an order");jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": ["node"]
},
"include": ["*.js"]
} Gotchas
- The
nevermessage means “unknown event”.Argument of type '() => void' is not assignable to parameter of type 'never'on anon()call says the event name is not in the map. errorevents are special at runtime. Node.js throws when an"error"event has no listener; adderror: [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
EventEmittersees untyped events again; pass it asOrderEvents.
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
- Events (class EventEmitter) · Node.js documentation
- @types/node · npm
Last reviewed 21 October 2026.