Event-driven архитектура на TypeScript: пишем гибкий код на событиях

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Event-driven архитектура на TypeScript: когда код «слышит» друг друга

Представьте, что вы пишете сложное веб-приложение: мессенджер, торгового бота или систему мониторинга серверов. В какой-то момент вы понимаете: просто вызывать функции по цепочке — это ад. Один модуль тянет за собой другой, третий завязан на четвёртый, а при изменении логики всё рассыпается как карточный домик.

Знакомо? Тогда вам нужна event-driven архитектура на TypeScript. Это не просто модный термин, а реальный способ сделать код гибким, расширяемым и понятным. В этой статье мы разберём, как работает событийно-ориентированное программирование, напишем свою шину событий на TypeScript и посмотрим, где это реально выручает.

Что такое Event-driven архитектура простыми словами

Event-driven архитектура (событийно-ориентированная архитектура) — это подход, при котором компоненты системы общаются через события. Один модуль генерирует событие (например, «пользователь зарегистрировался»), а другие подписываются на него и реагируют. Между собой они напрямую не связаны.

Звучит абстрактно? Давайте на примере из жизни. Представьте офис:

  • Традиционный подход: начальник лично подходит к каждому сотруднику и говорит: «Сделай отчёт».
  • Event-driven: начальник кричит: «Нужен отчёт!» — и все, кто умеет его делать, сами берутся за работу.

Во втором случае начальнику не нужно знать, кто именно будет делать отчёт. Он просто отправляет событие. А сотрудники могут подключаться и отключаться без изменения логики начальника. Это и есть суть событийно-ориентированного программирования.

Почему TypeScript идеален для событийной архитектуры

JavaScript сам по себе событийно-ориентирован (вспомните addEventListener), но в больших проектах без типов начинается хаос. TypeScript события решают эту проблему: вы точно знаете, какие данные приходят в событии, и компилятор поймает ошибку на этапе разработки, а не в рантайме.

Кроме того, TypeScript поддерживает дженерики, перегрузки и строгую типизацию колбэков — всё, что нужно для создания надёжной шины событий.

Пишем Event Bus на TypeScript с нуля

Начнём с простого, но рабочего примера. Создадим класс EventBus — центральную шину событий, через которую будут общаться все модули.

// event-bus.ts
type EventHandler<T = any> = (data: T) => void;

class EventBus {
  private handlers: Map<string, EventHandler[]> = new Map();

  // Подписка на событие
  on<T>(event: string, handler: EventHandler<T>): void {
    if (!this.handlers.has(event)) {
      this.handlers.set(event, []);
    }
    this.handlers.get(event)!.push(handler as EventHandler);
  }

  // Отписка от события
  off(event: string, handler: EventHandler): void {
    const handlers = this.handlers.get(event);
    if (handlers) {
      this.handlers.set(event, handlers.filter(h => h !== handler));
    }
  }

  // Эмиссия события
  emit<T>(event: string, data: T): void {
    const handlers = this.handlers.get(event);
    if (handlers) {
      handlers.forEach(handler => handler(data));
    }
  }
}

export const globalEventBus = new EventBus();

Вот мы и написали простую event-driven архитектуру на TypeScript. Теперь любой модуль может импортировать globalEventBus и либо слушать события, либо генерировать их.

Пример использования: уведомления и логирование

Представим, что у нас есть модуль аутентификации и модуль уведомлений. Они не должны знать друг о друге напрямую.

// auth-module.ts
import { globalEventBus } from './event-bus';

export function loginUser(email: string): void {
  // ... логика входа
  console.log(`Пользователь ${email} вошёл в систему`);

  // Генерируем событие
  globalEventBus.emit('user:login', { email, timestamp: Date.now() });
}
// notification-module.ts
import { globalEventBus } from './event-bus';

export function initNotifications(): void {
  globalEventBus.on('user:login', (data: { email: string; timestamp: number }) => {
    console.log(`📧 Отправляем приветственное письмо на ${data.email}`);
    // Здесь мог быть вызов API
  });

  globalEventBus.on('user:logout', () => {
    console.log('👋 Пока-пока!');
  });
}

Обратите внимание: модуль аутентификации ничего не знает про уведомления. Он просто «кричит» о событии. А модуль уведомлений подписывается на нужные ему события. Это и есть событийно-ориентированное программирование в действии.

Продвинутая типизация: делаем события типобезопасными

В примере выше мы использовали any в EventHandler, но это не совсем по-тайпскриптовски. Давайте сделаем строгую типизацию, чтобы компилятор контролировал все типы данных для каждого события.

// typed-event-bus.ts
// Сначала определим все возможные события и их типы данных
interface AppEvents {
  'user:login': { email: string; timestamp: number };
  'user:logout': { userId: string };
  'order:created': { orderId: string; amount: number };
  'error:occurred': { message: string; code: number };
}

type EventKey = keyof AppEvents;

class TypedEventBus {
  private handlers: Map<EventKey, Function[]> = new Map();

  on<E extends EventKey>(event: E, handler: (data: AppEvents[E]) => void): void {
    if (!this.handlers.has(event)) {
      this.handlers.set(event, []);
    }
    this.handlers.get(event)!.push(handler);
  }

  emit<E extends EventKey>(event: E, data: AppEvents[E]): void {
    const handlers = this.handlers.get(event);
    if (handlers) {
      handlers.forEach(handler => handler(data));
    }
  }
}

export const typedBus = new TypedEventBus();

// Пример строгой типизации
typedBus.on('user:login', (data) => {
  // data.email — строка, data.timestamp — число. Всё под контролем!
  console.log(data.email);
});

// А вот так уже не скомпилируется:
// typedBus.emit('user:login', { email: 123 }); // Ошибка! email должен быть строкой

Теперь наша шина событий полностью типобезопасна. Если вы ошиблись с типом данных при эмиссии — TypeScript укажет на ошибку ещё до запуска.

Когда использовать Event-driven архитектуру, а когда — нет

Event-driven архитектура на TypeScript — мощный инструмент, но не панацея. Давайте честно разберём плюсы и минусы.

Плюсы:

  • Слабая связанность (loose coupling

Блоги

Рекомендаций по книгам