Условные типы в TypeScript ценны не сами по себе, а тем, что позволяют переносить логику из кода в систему типов. Вместо грубого “этот generic что-то возвращает” ты можешь описать правило: если входной тип соответствует одному условию, верни один результат, иначе другой.

Это особенно полезно в библиотеках, API-слоях и утилитах, где тип должен зависеть от входного значения. Без conditional types код быстро скатывается либо в дублирование перегрузок, либо в слишком широкий any, который уже ничего не гарантирует.

Ниже разберем базовый синтаксис, распределительное поведение и пример, где conditional types действительно заменяют несколько overload-объявлений.

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

Условный тип выглядит как тернарный оператор, только на уровне типов:

type Container<T> = T extends string ? StringContainer : NumberContainer;

interface StringContainer {
  value: string;
  format(): string;
}

interface NumberContainer {
  value: number;
  round(): number;
}

Если T является string, тип Container<T> превращается в StringContainer. Во всех остальных случаях - в NumberContainer.

type StringResult = Container<string>;
type NumberResult = Container<number>;

const first: StringResult = {
  value: "Alex",
  format() {
    return this.value.toUpperCase();
  },
};

const second: NumberResult = {
  value: 3.14,
  round() {
    return Math.round(this.value);
  },
};

На практике такой код полезен там, где форма результата зависит от входного generic-параметра.

Распределительные условные типы

Если слева от extends стоит “голый” generic-параметр T, условный тип автоматически распределяется по union-типу.

Например, отфильтруем из union только массивы:

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

type Mixed = string | number | string[] | number[];
type ArraysFromMixed = ArrayOnly<Mixed>;
// string[] | number[]

Что здесь происходит:

  1. TypeScript применяет условие отдельно к каждому члену union.
  2. Получается never | never | string[] | number[].
  3. never исчезает из union.
  4. Остается string[] | number[].

На этом поведении построены многие встроенные утилиты TypeScript, например Exclude и Extract.

Использование infer

infer позволяет вытащить часть типа прямо внутри conditional type. Это один из самых мощных паттернов в современном TypeScript.

Например, можно получить тип элемента массива:

type ElementOf<T> = T extends readonly (infer Item)[] ? Item : never;

type User = {
  id: string;
};

type UserArray = User[];
type UserItem = ElementOf<UserArray>;
// User

Или извлечь тип результата из промиса:

type AwaitedValue<T> = T extends Promise<infer Value> ? Value : T;

type A = AwaitedValue<Promise<number>>;
type B = AwaitedValue<string>;
// A = number
// B = string

Если ты когда-либо писал типовую утилиту “достань внутренний тип из контейнера”, почти наверняка тебе нужен именно infer.

Когда conditional types заменяют overloads

Перегрузки работают, но быстро разрастаются. Если правило выбора результата простое и выражается через тип, conditional type обычно читается лучше.

Допустим, у нас есть сервис, который по строковому id возвращает Book, а по числовому - Movie:

interface Book {
  id: string;
  title: string;
}

interface Movie {
  id: number;
  duration: number;
}

type EntityById<T extends string | number> = T extends string ? Book : Movie;

interface ItemService {
  getItem<T extends string | number>(id: T): EntityById<T>;
}

Теперь тип результата зависит от типа аргумента:

declare const itemService: ItemService;

const book = itemService.getItem("book-1");
const movie = itemService.getItem(42);

// book -> Book
// movie -> Movie

Это хороший пример замены overloads, потому что правило простое и линейное.

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

Итог

Conditional types полезны тогда, когда тип результата действительно зависит от входного типа. Они помогают убирать дублирование, строить утилиты поверх generics и держать API точным без ручного перечисления всех вариантов.

Если нужен практический ориентир, начинай с простого вопроса: “Могу ли я описать это как правило если T такое, верни X, иначе Y?” Если да, conditional type почти наверняка подойдет. Если правило уже сложно объяснить словами, возможно, лучше оставить явные overloads или разбить тип на несколько частей.