TypeScript Условные типы

Условные типы в TypeScript позволяют создавать типы, зависящие от других типов — аналогично тому, как работают конструкции if‑else в JavaScript.

Это мощная возможность, открывающая путь к сложным преобразованиям типов и программированию на уровне типов.


Ключевые концепции

  • Логика на уровне типов: выполнение условных проверок для типов.
  • Вывод типов: извлечение и преобразование типов с помощью infer.
  • Композиция: сочетание с другими возможностями TypeScript.
  • Вспомогательные типы: создание мощных утилит для работы с типами.

Типичные сценарии применения

  • Типобезопасная перегрузка функций.
  • Преобразование типов ответов API.
  • Сложная валидация типов.
  • Создание повторно используемых утилит для работы с типами.
  • Продвинутый вывод типов.

Базовый синтаксис условных типов

Условные типы используют форму T extends U ? X : Y, что означает:

"Если тип T расширяет (или совместим с) тип U, использовать тип X, иначе — тип Y."

Пример


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

// Примеры использования
type Result1 = IsString<string>; // true
type Result2 = IsString<number>; // false
type Result3 = IsString<"hello">;// true (литеральные типы расширяют свои базовые типы)

// Также можно использовать с переменными
let a: IsString<string>;  // a имеет тип 'true'
let b: IsString<number>;  // b имеет тип 'false'


Условные типы с объединениями

Распределяющие условные типы

Условные типы особенно полезны с типами‑объединениями: они автоматически применяются к каждому элементу объединения.

Пример


type ToArray<T> = T extends any ? T[] : never;
// При использовании с типом‑объединением применяется к каждому его элементу
type StringOrNumberArray = ToArray<string | number>;
// Превращается в ToArray<string> | ToArray<number>
// Что даёт string[] | number[]


// Можно также извлекать конкретные типы из объединения
type ExtractString<T> = T extends string ? T : never;
type StringsOnly = ExtractString<string | number | boolean | "hello">;
// Результат: string | "hello"


Вывод типов с infer

Извлечение типов из сложных структур

Ключевое слово infer позволяет объявить переменную типа внутри условия условного типа, а затем использовать её в "истинной" ветви условия.

Пример


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


// Примеры
function greet() { return "Hello, world!"; }
function getNumber() { return 42; }

type GreetReturnType = ReturnType<typeof greet>;    // string
type NumberReturnType = ReturnType<typeof getNumber>; // number


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


Встроенные условные типы

Утилиты стандартной библиотеки

TypeScript включает несколько встроенных условных типов в стандартной библиотеке:

Пример


// Extract<T, U> — извлекает из T типы, совместимые с U
type OnlyStrings = Extract<string | number | boolean, string>; // string

// Exclude<T, U> — исключает из T типы, совместимые с U
type NoStrings = Exclude<string | number | boolean, string>; // number | boolean


// NonNullable<T> — удаляет null и undefined из T
type NotNull = NonNullable<string | null | undefined>; // string

// Parameters<T> — извлекает типы параметров из функционального типа
type Params = Parameters<(a: string, b: number) => void>; // [string, number]

// ReturnType<T> — извлекает возвращаемый тип из функционального типа
type Return = ReturnType<() => string>; // string


Продвинутые паттерны и техники

Рекурсивные условные типы

Условные типы можно использовать рекурсивно для создания сложных преобразований типов.

Пример


// Глубоко "распаковываем" типы Promise
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;

// Примеры
type A = UnwrapPromise<Promise<string>>;           // string
type B = UnwrapPromise<Promise<Promise<number>>>; // number
type C = UnwrapPromise<boolean>;                  // boolean

Цепочки if-else на уровне типов

Можно объединять несколько условий в одну цепочку для сложной типовой логики.

Пример


type TypeName<T> =
  T extends string ? "string" :
  T extends number ? "number" :
  T extends boolean ? "boolean" :
  T extends undefined ? "undefined" :
  T extends Function ? "function" :
  "object";

// Использование
type T0 = TypeName<string>;     // "string"
type T1 = TypeName<42>;         // "number"
type T2 = TypeName<true>;       // "boolean"
type T3 = TypeName<() => void>;  // "function"
type T4 = TypeName<Date[]>;      // "object"

Условные типы особенно сильны при создании обобщённых утилит и типобезопасных библиотек.

Пример


// Функция, возвращающая разные типы в зависимости от типа входных данных
function processValue<T>(value: T): T extends string
  ? string
  : T extends number
  ? number
  : T extends boolean
  ? boolean
  : never {

  if (typeof value === "string") {
    return value.toUpperCase() as any; // Утверждение типа нужно из‑за ограничений
  } else if (typeof value === "number") {
    return (value * 2) as any;
  } else if (typeof value === "boolean") {
    return (!value) as any;
  } else {
    throw new Error("Unsupported type");
  }
}

// Использование
const stringResult = processValue("hello"); // Возвращает "HELLO" (тип — string)
const numberResult = processValue(10);       // Возвращает 20 (тип — number)
const boolResult = processValue(true);        // Возвращает false (тип — boolean)


Лучшие практики

Делайте:

  • Используйте условные типы для сложных преобразований типов.
  • Сочетайте с infer для извлечения типов.
  • Создавайте повторно используемые утилиты для работы с типами.
  • Документируйте сложные условные типы.
  • Тестируйте крайние случаи в определениях типов.

Не делайте:

  • Не злоупотребляйте сложными условными типами, если достаточно простых.
  • Не создавайте глубоко вложенные условные типы — они трудны для понимания.
  • Не забывайте о влиянии на производительность при очень сложных типах.
  • Не используйте условные типы для логики времени выполнения.

Вопросы производительности

  • Глубоко вложенные условные типы могут увеличить время компиляции.
  • Рассмотрите использование псевдонимов типов для промежуточных результатов.
  • Помните об ограничениях глубины рекурсии в TypeScript.