Наївна шина подій - on(event: string, handler: (data: any) => void): назву події легко переплутати, а дані не типізовані. Мета - щоб для кожної події тип даних виводився автоматично.
Карта подій як тип:
type AppEvents = {
'user:created': { id: number; email: string };
'cart:updated': { count: number };
'session:expired': void;
};
Типізована шина:
class EventBus<E extends Record<string, unknown>> {
private handlers: { [K in keyof E]?: Array<(payload: E[K]) => void> } = {};
on<K extends keyof E>(event: K, handler: (payload: E[K]) => void): () => void {
(this.handlers[event] ??= []).push(handler);
return () => {
this.handlers[event] = this.handlers[event]?.filter((h) => h !== handler);
};
}
emit<K extends keyof E>(event: K, ...args: E[K] extends void ? [] : [payload: E[K]]): void {
for (const handler of this.handlers[event] ?? []) {
handler(args[0] as E[K]);
}
}
}
const bus = new EventBus<AppEvents>();
bus.on('user:created', (user) => user.email); // user: { id; email }
bus.emit('cart:updated', { count: 3 });
bus.emit('session:expired'); // без аргументу
bus.emit('cart:updated', { count: '3' }); // помилка
bus.on('user:deleted', () => {}); // помилка: немає такої події
Що тут працює:
K extends keyof E- назва події з'єднує виклик з типом даних:E[K]- дані саме цієї події;- mapped type
{ [K in keyof E]?: ... }- сховище обробників, типізоване для кожної події окремо; - кортеж залишкових параметрів
E[K] extends void ? [] : [payload: E[K]]- для подій без даних аргумент не потрібен, для решти обов'язковий; - повернена функція відписки - зручно для
useEffectчиonUnmounted.
Варіації:
- шаблонні літеральні типи для груп подій:
Extract<keyof E, \cart:${string}`>` - підписка на всі події кошика; EventTarget+CustomEvent- браузерна реалізація; типізувати її можна тим самим прийомом через перевантаженняaddEventListener;- готові бібліотеки (
mitt,nanoevents) приймають таку саму карту подій генеріком.
Де ще працює цей патерн «карта типів + keyof»: типізовані маршрути API (Endpoints['GET /users']), повідомлення postMessage між вікнами чи воркерами, події WebSocket-каналів, ключі localStorage з типами значень.