Working with EventEmitter in TypeScript is painful. You get zero autocomplete, typos in event names fail silently, and your listeners accept any because the types don't flow through. I built this to fix that.
The goal is simple: Make event-driven code actually type-safe.
This package wraps EventEmitter so that every event name and payload is validated at compile time. No more guessing, no more runtime surprises.
on, emit, once you already know.bun add @nowarajs/typed-event-emitter
Define an event map interface, then pass it to TypedEventEmitter. Everything flows from there.
import { TypedEventEmitter } from '@nowarajs/typed-event-emitter';
interface MyEvents {
userLogin: [{ userId: string; timestamp: Date }];
userLogout: [{ userId: string }];
error: [Error];
}
const emitter = new TypedEventEmitter<MyEvents>();
// `payload` is inferred as { userId: string; timestamp: Date }
emitter.on('userLogin', (payload) => {
console.log(`User ${payload.userId} logged in at ${payload.timestamp}`);
});
// TypeScript will yell if you pass the wrong shape
emitter.emit('userLogin', {
userId: 'user123',
timestamp: new Date()
});
Events can have multiple arguments, and each one is typed individually.
import { TypedEventEmitter } from '@nowarajs/typed-event-emitter';
interface Events {
move: [x: number, y: number];
click: [button: 'left' | 'right', x: number, y: number];
}
const input = new TypedEventEmitter<Events>();
// x and y are both numbers
input.on('move', (x, y) => {
console.log(`Mouse moved to ${x}, ${y}`);
});
// button is 'left' | 'right', x and y are numbers
input.on('click', (button, x, y) => {
console.log(`${button} click at ${x}, ${y}`);
});
input.emit('move', 100, 200);
input.emit('click', 'left', 50, 75);
Full docs: nowarajs.github.io/typed-event-emitter
MIT - Feel free to use it.