Implementing a Strictly Typed Event Emitter
Owner: SnippetBot
Created: 2026-07-15 00:00:50
Size: 2.56 KB
Expires: Never
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/**
* Defines a strictly typed event emitter interface and a basic implementation.
* The `EventsMap` generic parameter ensures that event names correspond
* to their correct payload types, preventing runtime type errors.
*/
interface EventsMap {
'userLoggedIn': (userId: string) => void;
'productUpdated': (productId: string, newPrice: number) => void;
'appError': (error: Error) => void;
}
class TypedEventEmitter<M extends EventsMap> {
private listeners: {
[K in keyof M]?:
M[K][]; // Array of the specific function type for event K
} = {};
on<K extends keyof M>(eventName: K, listener: M[K]): void {
if (!this.listeners[eventName]) {
this.listeners[eventName] = [];
}
(this.listeners[eventName] as M[K][]).push(listener);
}
emit<K extends keyof M>(eventName: K, ...args: Parameters<M[K]>): void {
if (this.listeners[eventName]) {
(this.listeners[eventName] as M[K][]).forEach(listener => {
listener(...args);
});
}
}
off<K extends keyof M>(eventName: K, listener: M[K]): void {
if (!this.listeners[eventName]) return;
const index = (this.listeners[eventName] as M[K][]).indexOf(listener);
if (index > -1) {
(this.listeners[eventName] as M[K][]).splice(index, 1);
}
}
}
// Example Usage:
const appEvents = new TypedEventEmitter<EventsMap>();
const userLoginListener = (userId: string) => {
console.log(`User ${userId} logged in.`);
};
const productUpdateListener = (productId: string, newPrice: number) => {
console.log(`Product ${productId} updated to price: $${newPrice.toFixed(2)}.`);
};
const errorListener = (error: Error) => {
console.error(`Application error: ${error.message}`);
};
appEvents.on('userLoggedIn', userLoginListener);
appEvents.on('productUpdated', productUpdateListener);
appEvents.on('appError', errorListener);
appEvents.emit('userLoggedIn', 'jane.doe'); // OK
appEvents.emit('productUpdated', 'PROD-789', 29.99); // OK
appEvents.emit('appError', new Error('Something went wrong!')); // OK
// appEvents.emit('userLoggedIn', 123); // Type Error: Argument of type 'number' is not assignable to parameter of type 'string'.
// appEvents.emit('productUpdated', 'PROD-123'); // Type Error: Expected 2 arguments, but got 1.
// appEvents.emit('unknownEvent', 'data'); // Type Error: Argument of type '"unknownEvent"' is not assignable to parameter of type 'keyof EventsMap'.
appEvents.off('userLoggedIn', userLoginListener);
console.log('
Emitting after removing a listener:');
appEvents.emit('userLoggedIn', 'john.doe'); // This will not log anything as the listener was removed.