Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Як типізувати шину подій (event emitter), щоб назва події й дані були пов'язані?

Наївна шина подій - 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 з типами значень.

Докладніше в документації: Mapped types

Перевір себе

20 випадкових питань за спробу, після завершення - розбір кожної помилки

Схожі питання