Что такое conditional types в TypeScript?

SeniorTypeScript · Frontend·Обновлено 6 июля 2026
Коротко
Conditional types — это конструкция TypeScript, позволяющая выбирать тип на основе условия вида T extends U ? X : Y. Они работают аналогично тернарному оператору, но на уровне системы типов.

Conditional Types в TypeScript

Conditional types позволяют задавать типы, которые зависят от условия. Синтаксис аналогичен тернарному оператору:

T extends U ? X : Y

Если тип T совместим с типом U, результирующий тип — X, иначе — Y.

Базовое использование

Простой пример — определение типа на основе входного параметра:

type IsString<T> = T extends string ? true : false;

type A = IsString<string>;  // true
type B = IsString<number>;  // false

Distributive Conditional Types

Когда условный тип применяется к union-типу, он автоматически распределяется по каждому члену union. Это называется дистрибутивностью:

type ToArray<T> = T extends unknown ? T[] : never;

// Распределяется: string[] | number[], а не (string | number)[]
type Result = ToArray<string | number>;

Чтобы отключить дистрибутивность, нужно обернуть тип в кортеж:

type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;

// Теперь: (string | number)[]
type Result = ToArrayNonDist<string | number>;

Ключевое слово infer

infer позволяет извлекать типы прямо внутри условного типа, объявляя переменную типа в позиции extends:

// Извлекаем тип возвращаемого значения функции
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Fn = (x: number) => string;
type FnReturn = ReturnType<Fn>; // string

// Извлекаем тип элемента массива
type UnpackArray<T> = T extends (infer U)[] ? U : T;

type Unpacked = UnpackArray<number[]>; // number

Встроенные утилиты на основе conditional types

Многие стандартные утилиты TypeScript реализованы через conditional types:

// Exclude — исключает из T типы, совместимые с U
type Exclude<T, U> = T extends U ? never : T;

type T1 = Exclude<string | number | boolean, string>; // number | boolean

// Extract — оставляет только типы из T, совместимые с U
type Extract<T, U> = T extends U ? T : never;

type T2 = Extract<string | number | boolean, string>; // string

// NonNullable — убирает null и undefined
type NonNullable<T> = T extends null | undefined ? never : T;

Рекурсивные conditional types

Начиная с TypeScript 4.1, conditional types поддерживают рекурсию:

// Рекурсивное извлечение типа из вложенных массивов
type DeepUnpack<T> = T extends (infer U)[] ? DeepUnpack<U> : T;

type Deep = DeepUnpack<number[][][]>; // number

Практический паттерн: фильтрация ключей объекта

// Оставляем только ключи, значения которых — строки
type StringKeys<T> = {
  [K in keyof T]: T[K] extends string ? K : never;
}[keyof T];

interface User {
  name: string;
  age: number;
  email: string;
}

type UserStringKeys = StringKeys<User>; // 'name' | 'email'

Что хочет услышать интервьюер

Знание синтаксиса `T extends U ? X : Y` и понимание аналогии с тернарным оператором

Объяснение дистрибутивности conditional types при работе с union-типами и способ её отключения через кортеж

Уверенное владение ключевым словом `infer` для извлечения вложенных типов

Понимание, как реализованы стандартные утилиты (`ReturnType`, `Exclude`, `Extract`, `NonNullable`) через conditional types

Знание ограничений: рекурсия, отложенное вычисление (deferred), поведение в дженериках

Пример: Базовый синтаксис

// Базовый синтаксис
type IsArray<T> = T extends any[] ? true : false;

type A = IsArray<string[]>; // true
type B = IsArray<string>;   // false

Пример: Ключевое слово infer

// infer — извлечение типа изнутри
type UnpackPromise<T> = T extends Promise<infer U> ? U : T;

type Resolved = UnpackPromise<Promise<number>>; // number
type Plain    = UnpackPromise<string>;          // string

Пример: Дистрибутивность и её отключение

// Дистрибутивность: условный тип распределяется по union
type Wrap<T> = T extends unknown ? { value: T } : never;

// { value: string } | { value: number } — не { value: string | number }
type Wrapped = Wrap<string | number>;

// Отключение дистрибутивности через кортеж
type WrapNonDist<T> = [T] extends [unknown] ? { value: T } : never;
// { value: string | number }
type WrappedSingle = WrapNonDist<string | number>;

Пример: Фильтрация ключей объекта

// Фильтрация ключей объекта по типу значения
type KeysOfType<Obj, ValueType> = {
  [K in keyof Obj]: Obj[K] extends ValueType ? K : never;
}[keyof Obj];

interface Config {
  host: string;
  port: number;
  debug: boolean;
  name: string;
}

// 'host' | 'name'
type StringProps = KeysOfType<Config, string>;

Типичные ошибки

Не понимают дистрибутивность — удивляются, что `T extends unknown ? T[] : never` для union даёт массив каждого члена, а не массив union

Путают `infer` с обычной переменной типа — пытаются использовать `infer` вне extends-клаузулы

Забывают, что conditional type в дженерике вычисляется отложенно (deferred) и не разрешается до передачи конкретного типа

Неправильно используют `never` в условных типах — не понимают, что `never` в union просто исчезает

Не знают, как отключить дистрибутивность через обёртку в кортеж `[T] extends [U]`

Лучшие курсы по теме

изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Feature-Sliced Design

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.5
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее