Типизация WebSocket событий: как discriminated unions + brand checks делают стейт-машин...
Типизация WebSocket событий: как discriminated unions + brand checks делают стейт-машину предсказуемой и спасают от багов с payload Каждый, кто писал real-time фичи на React или Vue, сталкивался: диспатчишь событие WebSocket, а payload — не того типа. Структура похожа, семантика разная, и TypeScript молчит. Ошибка уходит в production. Discriminated unions — база, но не панацея Стандартный подход: поле type сужает остальные поля. Но если два события содержат sessionId с разным смыслом, TS не заметит подмены. На уровне типов всё ок, в рантайме — треш. Brand checks — второй рубеж Добавьте фиктивное поле __brand: 'open' в payload. В рантайме его нет, типы не гоняют лишние данные. Компилятор не даст смешать payload разных событий. Это имитация номинативной типизации в структурной системе TypeScript. Стейт-машина перестаёт принимать невесть что: переходы становятся предсказуемыми. Пример типизации: type WSMessage = | { type: 'OPEN'; payload: { sessionId: string } & { __brand: 'open' } } | { type: 'MSG'; payload: { userId: number; text: string } & { __brand: 'msg' } } | { type: 'ERROR'; payload: { error: string } & { __brand: 'err' } }; Типичная ошибка Думать, что brand checks — это всё. WebSocket может прислать что угодно. Типы не панацея. В production обязательно докидывайте валидацию схемы на каждое входящее сообщение через Zod или io-ts. Практический совет Используйте brand checks как инструмент для разработчика, чтобы не протащить payload не туда при сборке стейта. А рантайм-валидацию — как первую линию защиты от кривых данных с сервера. Вместе они дают надёжность и предсказуемость в real-time архитектуре. Вывод: Discriminated unions сужают типы по значению, brand checks защищают от семантических багов, а рантайм-валидация — от любых неожиданностей с сервера: три уровня защиты для production-grade стейт-машины.