Discriminated unions удобны в TypeScript, но в production их часто приходится проверять вручную через цепочки if и
switch. Это ломает типы и множит ошибки на границе с внешними данными — API-ответами, JSON, AST-нодами. Решение: combine mapped types с runtime для автоматической генерации type guards.Mapped type как контракт для runtime
Вместо того чтобы писать отдельный guard для каждого варианта union, определяем объект-гард через mapped type:
type ShapeGuards = {
[K in Shape['kind']]: (obj: unknown) => obj is Extract<Shape, { kind: K }>
};Теперь для каждой ветки (
circle, rect, group) нужно только реализовать валидацию полей. Тип обяжет вернуть правильный предикат.Единая точка входа и generic фабрика
Добавляем общую функцию:
function isShape<T extends Shape['kind']>(kind: T, obj: unknown): obj is Extract<Shape, { kind: T }> {
return shapeGuards[kind](obj);
}Типизация работает полностью: после
isShape('circle', data) TypeScript знает, что data — это { kind: 'circle'; radius: number }. Для рекурсивных веток вроде group с вложенными Shape[] это тоже безопасно.Можно уйти дальше — сделать generic-фабрику, которая возвращает строго типизированный guard для конкретного kind без кастингов внутри:
type GuardOfKind<T> = T extends Shape['kind']
? (obj: unknown) => obj is Extract<Shape, { kind: T }>
: never;
function createKindGuard<T extends Shape['kind']>(kind: T): GuardOfKind<T> {
return shapeGuards[kind] as any;
}
Типичная ошибка и trade-off
Ошибка: проверять только
kind, а не поля варианта. TypeScript поведет, но в runtime придет некорректный объект — и сломается что-то ниже. Всегда проверяй специфичные поля: radius, shapes и т.д.Минус подхода: для глубоко вложенных дифференцированных union придется писать много ручной валидации. Лечится либо кодогенерацией, либо внешними библиотеками (zod, io-ts). Но для легаси с 10+ ветками такой подход выигрывает у хаоса из
switch и if-else.Вывод: Соединение conditional типов с runtime guards превращает абстрактную типизацию в production-ready механизм проверки на границе доверия — особенно в API-клиентах, SDK и парсерах AST.