Внедрение условных типов (Conditional Types) в версии TypeScript 2.8 ознаменовало превращение системы типов в Тьюринг-полный функциональный язык программирования, исполняемый на этапе статической компиляции. Условные типы позволяют выражать нелинейные зависимости между входными типами и результирующей структурой данных, выбирая одну из двух ветвей вычисления на основе отношения подтипирования.
Синтаксис условного типа повторяет тернарный оператор JavaScript:
type Result = T extends U ? TrueBranch : FalseBranch;Если тип T является подтипом типа U (T <: U), вычисляется ветвь TrueBranch, в противном случае — FalseBranch.
Два фундаментальных механизма расширяют выразительную мощность условных типов:
Union типа, если проверяемый тип является "голым" параметром типа (naked type parameter).infer: механизм сопоставления с образцом (Pattern Matching), позволяющий объявить переменную типа прямо внутри блока extends и извлечь вложенный структурный подтип.Связка условных типов и infer лежит в основе всех современных архитектур метапрограммирования: от стандартных утилит ReturnType, Parameters и Awaited до сложных парсеров контрактов RPC и ORM.
При вычислении условного типа компилятор анализирует природу операнда T. Если T — это ненаправленный параметр типа (naked type parameter), а на вход передано объединение A | B | C, операция раскрывается дистрибутивно:
+-------------------------------------------------------------------------+
| ДИСТРИБУТИВНОСТЬ УСЛОВНЫХ ТИПОВ |
| (A | B) extends U ? TrueBranch : FalseBranch |
+-------------------------------------------------------------------------+
| | |
| Раскрывается компилятором в объединение ветвей: |
| v |
| (A extends U ? TrueBranch : FalseBranch) |
| | (Union) |
| (B extends U ? TrueBranch : FalseBranch) |
+-------------------------------------------------------------------------+
| |
| ОТКЛЮЧЕНИЕ ДИСТРИБУТИВНОСТИ ЧЕРЕЗ КОРТЕЖ [T] |
| [A | B] extends [U] ? TrueBranch : FalseBranch |
| | |
| v |
| Проверяет всё объединение (A | B) как монолитную сущность |
+-------------------------------------------------------------------------+Ключевое слово infer выступает в роли "детектора шаблона": компилятор сопоставляет входящий тип с целевой формой и связывает неизвестную позицию с новым идентификатором типа.
Компилятор анализирует тип T: если передан Union и T не обернут в кортеж, активируется дистрибутивное расщепление.
Условное выражение дублируется для каждого отдельного элемента объединения.
Для каждой ветви структура типа проверяется на совпадение с шаблоном, захватывая неизвестные переменные типов.
Все вычисленные ветви объединяются обратно в результирующий нормализованный Union тип.
Почему выражение T extends never ? true : false возвращает never, если T = never, и как отключить такое поведение?
infer без необходимости ручного дублирования интерфейсов ответов.Promise<Promise<string[]>> в плоский тип string[] при обработке сетевых событий.type SafePayload<T> = T extends AppError ? never : T.Payload из функций генерации действий ActionCreator<T>.Компилятор проверяет, является ли T голым параметром. Если передан Union (A | B), запускается дистрибутивный цикл по каждому члену.
Структура T сопоставляется с правой частью extends. Если в шаблоне есть infer R, компилятор извлекает тип в переменную R.
Если одна переменная R захватывается в нескольких местах, компилятор объединяет их через Union (вывод) или Intersection (ввод).
Подстановка захваченного типа R в TrueBranch или переход в FalseBranch. Финальная нормализация типа.
never#Тип never в TypeScript математически представляет собой пустое объединение ($0$ членов). Из-за этого вычисление условного типа над never ведет себя не так, как ожидают многие разработчики:
// ❌ НАИВНАЯ ОШИБОЧНАЯ РЕАЛИЗАЦИЯ:
export type IsNeverWrong<T> = T extends never ? true : false;
// Почему IsNeverWrong<never> возвращает never, а не true?
// Поскольку never — это ПУСТОЕ объединение, цикл дистрибутивности выполняется 0 раз!
// Результат: пустое объединение => never!
type Test1 = IsNeverWrong<never>; // never!
// ✅ ПРАВИЛЬНАЯ РЕАЛИЗАЦИЯ: отключение дистрибутивности через кортеж [T]
export type IsNever<T> = [T] extends [never] ? true : false;
type Test2 = IsNever<never>; // true!
type Test3 = IsNever<string>; // false!
type Test4 = IsNever<unknown>; // false!Отключение дистрибутивности также необходимо, когда требуется проверить, является ли тип объединением:
// Проверка: является ли тип Union-объединением?
// Трюк: [T] отключает дистрибутивность во внешней части,
// в то время как T внутри остается голым и распределяется!
export type IsUnion<T, U = T> = T extends any
? [U] extends [T]
? false
: true
: never;
type Case1 = IsUnion<string>; // false
type Case2 = IsUnion<string | number>; // true
type Case3 = IsUnion<"a" | "b" | "c">; // true
type Case4 = IsUnion<[string, number]>; // false (кортеж монолитен)infer: Извлечение параметров и возвращаемых типов#Ключевое слово infer позволяет производить деструктуризацию любых функциональных контрактов:
// 1. Извлечение типа первого аргумента функции
export type FirstArgument<TFunction> = TFunction extends (
first: infer TFirst,
...rest: readonly unknown[]
) => unknown
? TFirst
: never;
// 2. Извлечение типа последнего элемента массива/кортежа
export type LastElement<TTuple extends readonly unknown[]> = TTuple extends readonly [
...unknown[],
infer TLast
]
? TLast
: never;
// 3. Извлечение типа разрешенного значения асинхронного метода
export type AsyncReturnType<TFunction extends (...args: readonly unknown[]) => unknown> =
TFunction extends (...args: readonly unknown[]) => Promise<infer TResult>
? TResult
: ReturnType<TFunction>;
// Тестовые проверки
async function fetchUserMetrics(userId: string) {
return { userId, score: 98.5, active: true };
}
type TargetFirst = FirstArgument<typeof fetchUserMetrics>; // string
type TargetReturn = AsyncReturnType<typeof fetchUserMetrics>; // { userId: string, score: number, active: boolean }
type TargetTuple = LastElement<[number, string, boolean]>; // booleanAwaited<T>#Начиная с TypeScript 4.5, стандартный тип Awaited<T> реализован через глубоко рекурсивный условный тип, способный разворачивать цепочки PromiseLike объектов произвольной глубины с защитой от циклов:
// Архитектурная реализация рекурсивного разворачивателя Thenable-объектов
export type DeepAwaited<T> = T extends null | undefined
? T // Если передан null/undefined, возвращаем как есть
: T extends object & { then(onfulfilled: infer F, ...args: readonly unknown[]): unknown }
? F extends (value: infer V, ...args: readonly unknown[]) => unknown
? DeepAwaited<V> // Рекурсивный спуск в значение резолва
: never
: T; // Базовый тип, если T не является промисом
// Демонстрация глубокого разворачивания
type Level0 = DeepAwaited<string>; // string
type Level1 = DeepAwaited<Promise<string>>; // string
type Level2 = DeepAwaited<Promise<Promise<number>>>; // number
type Level3 = DeepAwaited<Promise<Promise<Promise<boolean>>>>; // booleaninfer при множественных совпадениях#Если одна и та же переменная infer R объявлена в нескольких позициях внутри шаблона, компилятор применяет законы теории вариативности:
|).&).// Контравариантная позиция: аргументы функции объединяются в Intersection (&)
type UnionToIntersection<U> = (
U extends unknown ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never;
type DisjointUnion = { role: "admin" } | { permissions: string[] };
// Магия теории типов: объединение трансформируется в пересечение!
type Intersected = UnionToIntersection<DisjointUnion>;
// Результат: { role: "admin" } & { permissions: string[] }Разработаем промышленную утилиту метапрограммирования, извлекающую метаинформацию о любой переданной функции (синхронной или асинхронной):
export type FunctionDescriptor<TFunction> = TFunction extends (
this: infer TThis,
...args: infer TArgs
) => infer TReturn
? {
thisContext: TThis;
parameters: TArgs;
parameterCount: TArgs extends { length: infer L } ? L : 0;
rawReturnType: TReturn;
isAsync: TReturn extends Promise<unknown> ? true : false;
resolvedReturnType: TReturn extends Promise<infer TResolved>
? TResolved
: TReturn;
}
: never;
// Демонстрация использования интроспектора
class PaymentService {
async processTransaction(this: PaymentService, orderId: string, amount: number) {
return { transactionId: "tx-42", status: "COMPLETED" as const };
}
}
export type PaymentMethodInfo = FunctionDescriptor<PaymentService["processTransaction"]>;
/*
Тип PaymentMethodInfo автоматически вычисляется компилятором в:
{
thisContext: PaymentService;
parameters: [orderId: string, amount: number];
parameterCount: 2;
rawReturnType: Promise<{ transactionId: string; status: "COMPLETED" }>;
isAsync: true;
resolvedReturnType: { transactionId: string; status: "COMPLETED" };
}
*/Что произойдет, если одна и та же переменная infer R захватывается одновременно в аргументах двух разных функций?
Поскольку never является пустым объединением, конструкция T extends never дистрибутивно вычисляется 0 раз и безусловно возвращает never. Любая проверка на never обязана экранироваться квадратными скобками [T] extends [never].
// ❌ СЛОМАНО: всегда возвращает never при передаче never
type CheckNever<T> = T extends never ? "is-never" : "not-never";
type Result1 = CheckNever<never>; // never!
// ✅ ИСПРАВЛЕНО: недистрибутивная проверка через кортежи
type CheckNeverSafe<T> = [T] extends [never] ? "is-never" : "not-never";
type Result2 = CheckNeverSafe<never>; // "is-never"Если условный тип оборачивает параметр в массив, дистрибутивность приведет к генерации объединения массивов (A[] | B[]) вместо массива объединений ((A | B)[]), что кардинально ломает дальнейшую работу с коллекциями.
// ❌ ОШИБКА: превращает string | number в string[] | number[]
type ToArrayDistributive<T> = T extends unknown ? T[] : never;
type MixedList1 = ToArrayDistributive<string | number>; // string[] | number[]
// ✅ ПРАВИЛЬНО: кортежная изоляция сохраняет целостность объединения
type ToArrayMonolithic<T> = [T] extends [unknown] ? T[] : never;
type MixedList2 = ToArrayMonolithic<string | number>; // (string | number)[]Ключевое слово infer разрешено использовать исключительно внутри условия extends. Попытка использовать infer в теле интерфейса, типе параметров дженерика или в типе возвращаемого значения напрямую вызовет синтаксическую ошибку TS1338.
// ❌ СИНТАКСИЧЕСКАЯ ОШИБКА TS1338: 'infer' declarations are only permitted in the 'extends' clause
type BadExtract<T> = infer R;
// ✅ ПРАВИЛЬНО: infer строго внутри правой части extends
type GoodExtract<T> = T extends infer R ? R : never;При неаккуратной реализации infer над функциями без возвращаемого значения тип void может непреднамеренно приводиться к undefined или never, нарушая контракт обработчиков событий.