В TypeScript условные типы по умолчанию распространяются по объединениям (
A | B). Это часто полезно, но иногда приводит к неожиданным результатам. Простое и надёжное решение — спрятать параметр в кортеж: [T] extends [X] ? ...🧩 Проблема:
Условный тип вида
T extends X ? A : B при подстановке T = A | B автоматически разбивается и проверяется для каждого члена объединения отдельно. Такой механизм называется distributive conditional types.
type IsString<T> = T extends string ? "yes" : "no";
type R = IsString<string | number>; // "yes" | "no"
Ожидание «всё или ничего» (весь тип — строка?) нарушается: вместо одного ответа получаем объединение ответов для частей. 😥
🧩 Решение:
Чтобы проверить весь тип одним выражением (не по частям), нужно «спрятать» T внутри другой структуры — самый простой приём: кортеж.
type IsStringStrict<T> = [T] extends [string] ? "yes" : "no";
type A = IsStringStrict<string>; // "yes"
type B = IsStringStrict<number>; // "no"
type C = IsStringStrict<string | number>; // "no" <- проверка по всему типу
Распределение происходит только при проверке «голого»
T. Оборачивая T в [T], мы запрещаем TypeScript разбирать объединение и заставляем его проверять тип целиком.🧩 Практический выбор:
✔️ Нужна логика для каждого члена union? — оставить поведение по умолчанию (распространение полезно);
✔️ Нужна единая проверка для всего типа? — спрятать
T ([T] extends [...] ? ...) и получить один итоговый результат.Как итог, осознание того, что условные типы по умолчанию работают с union поэлементно, а не целиком, заметно упрощает работу с типами. Приём с
[T] позволяет явно задать намерение, а именно проверить весь тип сразу и избежать неожиданных объединений в результате. 👍