TGViewer
Настоящий JavaScript Настоящий JavaScript @true_js · 5.89K subscribers
Post #3601 334
⁣Типизация FSM без библиотек: почему discriminated unions подводят в production и как это чинить

Discriminated unions в TypeScript отлично работают для статичных конечных автоматов, но в реальном production — с динамической сменой стейтов, асинхронными переходами и конфигами из плагинов — быстро проявляют лимиты. Частая ошибка: думать, что union решает все проблемы FSM, а потом ловить race conditions и невалидные переходы.

Закрытый union и динамические стейты
Union дискриминирован только на этапе компиляции. Если стейты подгружаются в рантайме — например, из конфига или через плагинную архитектуру — тип не расширить. Решение: карта стейтов через mapped types.

type StateMap = {
idle: Record<string, never>
loading: { progress: number }
error: { message: string }
}
type State = {
[K in keyof StateMap]: { status: K } & StateMap[K]
}[keyof StateMap]


Новый стейт в StateMap — и тип обновляется автоматически. Но типовая безопасность переходов остаётся хрупкой.

Нетипизированные переходы и гонки
DU типизирует только состояние, а не граф переходов. После loading можно прыгнуть в error, хотя бизнес-логика требует success. В production это даёт гонки при параллельных запросах. Попытка типизировать через guard-функции:

function transition<S extends State, T extends State>(
prev: S, next: T, guard: (prev: S) => boolean
): T {
if (!guard(prev)) throw new Error('Invalid transition')
return next
}


Но это не спасает от асинхронных race conditions — требуется флаг блокировки. Типы не гарантируют порядок, только композицию.

Контекст и дублирование полей
При 10+ стейтах общие поля (например, id заказа) дублируются в каждом типе. Вынос в дженерик — простое, но эффективное решение:

type BaseState<T extends string, Ctx> = { status: T } & Ctx
type FSM =
| BaseState<'idle', {}>
| BaseState<'loading', { progress: number }>
| BaseState<'error', { message: string }>


Так контекст не размазывается, а union остаётся читаемым.

Вывод: Discriminated unions — стартовая точка, но для production FSM без xstate обязательно нужны карта стейтов, типизированные переходы с guard'ами и вынесенный контекст, иначе на втором километре кода типы начнут врать при динамической смене состояний.
More from @true_js
  1. Oct 3, 2026Post #3963
  2. Oct 2, 2026😅 Айтишник отправил в одну компанию три одинаковых резюме и только одно дошло до финала О…
  3. Oct 2, 2026Post #3961
  4. Oct 2, 2026🤣 Правильно расставленные приоритеты в моей жизни би лайк: 💥 xCode Journal
  5. Oct 1, 2026🤯 Люди взбунтовались против «пыточной для ИИ» На GitHub заметили открытый проект AI Tortu…
  6. Oct 1, 2026День в Заонежье начинается ещё в дороге. Мы встретим вас в аэропорту или на вокзале. Дальш…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →