Впровадження умовних типів (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 виступає в ролі "детектора шаблону": компілятор зіставляє вхідний тип із цільовою формою та пов'язує невідому позицію з новим ідентифікатором типу.
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" };
}
*/Оскільки 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, порушуючи контракт обробників подій.
Компілятор аналізує тип T: якщо передано Union і T не обгорнутий у кортеж, активується дистрибутивне розщеплення.
Умовний вираз дублюється для кожного окремого елемента об'єднання.
Для кожної гілки структура типу перевіряється на збіг із шаблоном, захоплюючи невідомі змінні типів.
Усі обчислені гілки об'єднуються назад у результуючий нормалізований Union тип.