Conditional Types (условные типы) в TypeScript позволяют описывать один тип через другой по принципу T extends U ? X : Y. Такие типы дают мощные возможности при построении сложной типовой логики, особенно в библиотеках и декларациях API.
Пример базового условного типа:
type IsString<T> = T extends string ? 'yes' : 'no'; type A = IsString<string>; // 'yes' type B = IsString<number>; // 'no'
Если условный тип применяется к union-типу, то TypeScript "распределяет" условие по каждому элементу union отдельно. Это называется дистрибутивным поведением условных типов.
Пример:
type Foo<T> = T extends { id: number } ? string : boolean; type Result = Foo<{id: number} | {name: string}>; // string | boolean
Это очень мощная фича, но вызывает путаницу с ожидаемым результатом, особенно при работе с массивами и маппингом типов.
Оборачиваем тип в кортеж:
type NoDistrib<T> = [T] extends [{id: number}] ? string : boolean; type Result = NoDistrib<{id: number} | {name: string}>; // boolean
Вопрос: "Что произойдёт, если использовать условный тип с union-типами, не обернув в кортежи? Всегда ли результат — тот же, что при обычной логике?"
Ответ: Результат может быть неожиданным! Из-за дистрибутивности условие применяется к каждому члену union-типов отдельно. Чтобы строго сравнить весь union-тип, нужно использовать обертку (tuple).
Пример:
type Test<T> = T extends string ? number : boolean; type A = Test<string | boolean>; // number | boolean, а не просто boolean
История
В библиотеке для сериализации использовали условный тип для проверки структуры данных, но забыли обернуть дженерик-параметр в tuple. В результате на сложных union-типах декларации ломались, а компилятор выдавал непредсказуемые типы при использовании API.
История
При попытке реализовать типовую трансформацию для обработки полей моделей возникла потеря части информации: из-за того, что дистрибутивность порождает объединения, пару ветвей логики забыли обработать, и в итоге типизация стала слишком permissive.
История
Разработчик полагал, что T extends SomeType внутри conditional types поведет себя так, как мы ожидаем для всего объекта, но случилось "распыление" — компилятор указал на несогласованность, и начались тайп-хаос и серьёзные баги при генерации автодокументации.