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'ами и вынесенный контекст, иначе на втором километре кода типы начнут врать при динамической смене состояний.