В проектах с event-сорсингом и сагами магические строки для типов событий — частая причина production-багов. Одна опечатка в
'user_deleted' против 'user_deleted_at' — и рантайм ломает логику, которую TypeScript мог отловить на этапе компиляции. Senior-разработчики игнорируют это, пока не получают инцидент.Discriminated unions для событий
Вместо строкового enum или raw string используй union-тип с литеральным полем
type. TypeScript автоматически сужает payload в conditional-блоках:type UserEvent =
| { type: 'USER_CREATED'; payload: { id: string; name: string } }
| { type: 'USER_DELETED'; payload: { id: string } };
function handle(event: UserEvent) {
if (event.type === 'USER_CREATED') {
// payload: { id: string; name: string } — без as
console.log(event.payload.name);
}
}
Ошибка: хардкодить строку в каждом обработчике. Используй discriminated union, чтобы компилятор проверял, что payload соответствует типу.
Type-safe саги без магии
В саге, которая агрегирует события, union-тип предотвращает обращение к несуществующим типам:
type SagaEvent =
| { type: 'ORDER_CREATED'; payload: { orderId: string } }
| { type: 'PAYMENT_CONFIRMED'; payload: { paymentId: string } };
function processSaga(events: SagaEvent[]) {
// Ошибка: 'ORDER_DELETED' не входит в union
// events.find(e => e.type === 'ORDER_DELETED');
}
Практический совет: при добавлении нового события расширяй union явно — это заставит обновить все саги, где это событие участвует. Так ты не пропустишь ветку в switch или pattern-matching.
Trade-off: размер union
При 15-20 типах union становится громоздким. Но надежность перевешивает: компилятор гарантирует, что ты не используешь несуществующий тип или payload. Альтернатива с магическими строками — это deferred runtime failure, который проявится в production.
Вывод: Discriminated unions в event-сорсинге — это не избыточность, а контракт, который делает невозможным целый класс ошибок, связанных с опечатками и несоответствием payload.