Conditional Types

Conditional types — типы с условной логикой: T extends U ? X : Y. Позволяют выбирать тип в зависимости от условия.

Зачем нужно

  • Создание типов с условной логикой: «если T — строка, то X, иначе Y»
  • Извлечение вложенных типов через infer
  • Фильтрация union типов
  • Основа для многих встроенных utility types (ReturnType, Extract, Exclude)

Где используется

  • Утилитарные типы: Extract, Exclude, ReturnType, Parameters
  • Условная типизация возвращаемых значений
  • Извлечение типов из сложных структур
  • Типобезопасные API с перегрузками

Предпосылки

Базовый синтаксис

// T extends U ? X : Y
// Если T совместим с U → тип X, иначе → тип Y

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

type A = IsString<"hello">;  // true
type B = IsString<42>;        // false
type C = IsString<string>;    // true

// Практический пример
type TypeName<T> = T extends string
  ? "string"
  : T extends number
  ? "number"
  : T extends boolean
  ? "boolean"
  : T extends Function
  ? "function"
  : "object";

type A = TypeName<"hello">;    // "string"
type B = TypeName<42>;          // "number"
type C = TypeName< => void>; // "function"
type D = TypeName<{ x: 1 }>;  // "object"

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

infer объявляет переменную типа внутри условия, позволяя «извлечь» часть типа:

// Извлечь тип элемента массива
type ElementOf<T> = T extends (infer U) ? U : T;

type A = ElementOf<string>;   // string
type B = ElementOf<number>;   // number
type C = ElementOf<string>;     // string (не массив — возвращаем T)

// Извлечь тип из Promise
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<Promise<number>>; // number
type C = UnwrapPromise<string>;           // string

// Рекурсивная распаковка (как Awaited)
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;

type A = DeepUnwrap<Promise<Promise<Promise<string>>>>; // string

// Извлечь return type
type MyReturnType<T> = T extends (...args: any) => infer R ? R : never;

type A = MyReturnType< => string>;          // string
type B = MyReturnType<(x: number) => void>;   // void

// Извлечь параметры
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;

type A = MyParameters<(a: string, b: number) => void>; // [string, number]

// Извлечь первый параметр
type FirstParam<T> = T extends (first: infer F, ...rest: any) => any ? F : never;

type A = FirstParam<(name: string, age: number) => void>; // string

infer с constraints

// infer U extends string — U должен быть подтипом string
type GetString<T> = T extends { value: infer U extends string } ? U : never;

type A = GetString<{ value: "hello" }>; // "hello"
type B = GetString<{ value: 42 }>;       // never (42 не extends string)

Distributive Conditional Types

Когда conditional type применяется к union, он распределяется по каждому члену union:

type ToArray<T> = T extends any ? T : never;

// С union:
type A = ToArray<string | number>;
// (string extends any ? string : never) | (number extends any ? number : never)
// = string | number

// БЕЗ distributive (обернуть в кортеж):
type ToArrayNonDist<T> = [T] extends [any] ? T : never;

type B = ToArrayNonDist<string | number>;
// [string | number] extends [any] ? (string | number) : never
// = (string | number)

Фильтрация union через distribute

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

type A = Extract<"a" | "b" | "c" | 1 | 2, string>;
// "a" extends string → "a"
// "b" extends string → "b"
// "c" extends string → "c"
// 1 extends string → never
// 2 extends string → never
// Результат: "a" | "b" | "c"

// Exclude — убрать совместимые
type Exclude<T, U> = T extends U ? never : T;

type B = Exclude<"a" | "b" | 1 | 2, string>;
// Результат: 1 | 2

// NonNullable
type NonNullable<T> = T extends null | undefined ? never : T;

type C = NonNullable<string | null | undefined>;
// string

Практические примеры

Условный return type

// Перегрузка через conditional type
function process<T extends string | number>(
  value: T
): T extends string ? string : number {
  if (typeof value === "string") {
    return value.split("") as any;
  }
  return (value * 2) as any;
}

const a = process("hello"); // string
const b = process(42);       // number

Фильтрация свойств объекта по типу значения

// Ключи с определённым типом значения
type KeysOfType<T, V> = {
  [K in keyof T]: T[K] extends V ? K : never;
}[keyof T];

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
  isAdmin: boolean;
}

type StringKeys = KeysOfType<User, string>;
// "name" | "email"

type NumberKeys = KeysOfType<User, number>;
// "id" | "age"

// Pick только строковые поля
type StringFields = Pick<User, KeysOfType<User, string>>;
// { name: string; email: string }

Рекурсивный Flatten

type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;

type A = Flatten<number>;  // number
type B = Flatten<string>;       // string
type C = Flatten<number>;         // number

// Flatten объекта (на один уровень)
type FlattenObject<T> = {
  [K in keyof T]: T[K] extends object
    ? T[K] extends any
      ? T[K]
      : keyof T[K] extends never
      ? T[K]
      : T[K]
    : T[K];
};

Вычисление типа из конфигурации

interface FieldConfig {
  type: "string" | "number" | "boolean";
  required: boolean;
}

type FieldType<F extends FieldConfig> = (F["type"] extends "string"
  ? string
  : F["type"] extends "number"
  ? number
  : boolean) extends infer T
  ? F["required"] extends true
    ? T
    : T | undefined
  : never;

type A = FieldType<{ type: "string"; required: true }>;   // string
type B = FieldType<{ type: "number"; required: false }>;   // number | undefined
type C = FieldType<{ type: "boolean"; required: true }>;   // boolean

Частые ошибки

  1. Забыть про distributive behavior — union распределяется автоматически
type IsNever<T> = T extends never ? true : false;

type A = IsNever<never>; // never (не true!)
// never — пустой union, distribute ничего не даёт

// Правильно:
type IsNever<T> = [T] extends [never] ? true : false;
type B = IsNever<never>; // true
  1. Слишком глубокая рекурсия — TypeScript ограничивает глубину
  2. infer в неправильной позиции — infer работает только внутри extends в conditional type
  3. Путать extends в conditional и constraintT extends U ? X : Y (условие) vs T extends U (ограничение)
  4. Не использовать as any в реализации — TypeScript не может сузить тип внутри функции по conditional return

Практика

  1. Реализуйте MyReturnType<T> и MyParameters<T> через infer
  2. Создайте Flatten<T> — рекурсивно распаковывает массивы
  3. Напишите IsArray<T> — возвращает true если T массив, иначе false
  4. Реализуйте KeysOfType<T, V> — ключи объекта с определённым типом значения
  5. Создайте NonDistributive<T> — обёртка для отключения distributive behavior

Связанные темы

Ресурсы