Перейти к основному содержимому

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

Условный тип — это тип, который зависит от другого типа. Он работает как конструкция if-else, но для типов, а не для значений.


Как понимать условные типы в TypeScript

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

Ключевые понятия:

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

Частые случаи применения:

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

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

Условный тип имеет вид T extends U ? X : Y. Читается это так: если тип T расширяет (то есть совместим по присваиванию с) тип U, используется тип X, иначе — тип Y.

Пример

IsString<T> становится true для string и для литерала "hello", потому что литерал расширяет свой базовый тип. Для number получается false. Полученный тип можно использовать и для переменных.

Prompt
Покажите базовый синтаксис условного типа T extends U ? X : Y. Объявите IsString<T> как T extends string ? true : false, затем псевдонимы Result1, Result2 и Result3 для string, number и литерала "hello" с комментариями, указывающими true, false, true, и две переменные a: IsString<string> и b: IsString<number> с комментариями, поясняющими их типы 'true' и 'false'. Только типы, ничего не выводится.
Вывод: программа на TypeScript и захваченный вывод

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

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

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

ToArray<string | number> разбивается на ToArray<string> | ToArray<number>, что даёт string[] | number[]. Та же дистрибутивность позволяет типу ExtractString оставить только строковые члены объединения, отображая остальные в never.

Prompt
Покажите, как условный тип распределяется по членам объединения. Объявите ToArray<T> как T extends any ? T[] : never и StringOrNumberArray = ToArray<string | number> с комментариями, объясняющими, что получается string[] | number[], затем ExtractString<T>, который оставляет только строковые члены, и StringsOnly, применённый к string | number | boolean | "hello", с комментарием, указывающим string | "hello". Только типы, ничего не выводится.
Вывод: программа на TypeScript и захваченный вывод

Вывод типов с помощью infer

Ключевое слово infer объявляет типовую переменную внутри условия условного типа. Если условие совпадает, переменная хранит захваченный тип, и его можно использовать в ветке «истина».

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

ReturnType захватывает возвращаемый тип R типа функции. ElementType захватывает тип элемента U типа массива. С помощью typeof greet в эти помощники можно передать тип реальной функции.

Prompt
Покажите, как ключевое слово infer захватывает возвращаемый тип и тип элемента массива. Объявите ReturnType<T> как T extends (...args: any[]) => infer R ? R : never, две функции greet(), возвращающую "Hello, world!", и getNumber(), возвращающую 42, а также псевдонимы GreetReturnType/NumberReturnType с использованием typeof и комментариями, указывающими string и number. Затем объявите ElementType<T>, который выводит U из (infer U)[], и псевдонимы для number[] и string[]. Только типы, ничего не выводится; сделайте файл модулем, чтобы локальный ReturnType не конфликтовал с глобальным.
Вывод: программа на TypeScript и захваченный вывод

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

Стандартная библиотека TypeScript уже содержит несколько условных типов, поэтому самые распространённые из них писать самостоятельно не нужно.

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

Extract оставляет подходящие члены объединения, Exclude их удаляет, NonNullable убирает null и undefined, а Parameters и ReturnType разбирают тип функции на части.

Prompt
Покажите встроенные условные вспомогательные типы из стандартной библиотеки. Объявите OnlyStrings = Extract<string | number | boolean, string>, NoStrings = Exclude<string | number | boolean, string>, NotNull = NonNullable<string | null | undefined>, Params = Parameters<(a: string, b: number) => void> и Return = ReturnType<() => string>, каждый с комментарием, объясняющим помощник и получившийся тип. Только типы, ничего не выводится.
Вывод: программа на TypeScript и захваченный вывод

Продвинутые приёмы и шаблоны

Условные типы могут вызывать сами себя, выстраиваться в цепочку наподобие лестницы if-else и описывать возвращаемый тип обобщённой функции.

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

Условный тип может ссылаться на самого себя. UnwrapPromise продолжает разворачивать тип, пока тот не перестанет быть Promise, поэтому даже промис внутри промиса сводится к обычному внутреннему типу.

Prompt
Покажите рекурсивный условный тип, который полностью разворачивает типы Promise. Объявите UnwrapPromise<T> как T extends Promise<infer U> ? UnwrapPromise<U> : T, затем псевдонимы A, B и C для Promise<string>, Promise<Promise<number>> и boolean с комментариями, указывающими string, number и boolean. Только типы, ничего не выводится.
Вывод: программа на TypeScript и захваченный вывод

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

Несколько условий можно объединить в цепочку, одно за другим. TypeName по очереди сверяет тип с string, number, boolean, undefined и Function, а в остальных случаях возвращает "object".

Prompt
Покажите цепочку условных типов, которая работает как лестница if-else на уровне типов. Объявите TypeName<T>, который отображает string, number, boolean, undefined и Function в строковые литералы "string", "number", "boolean", "undefined" и "function", а в остальных случаях возвращает "object". Добавьте псевдонимы от T0 до T4 для string, 42, true, () => void и Date[] с комментариями, показывающими результаты. Только типы, ничего не выводится.
Вывод: программа на TypeScript и захваченный вывод

Условные возвращаемые типы

Условные типы особенно хороши в обобщённых утилитах и типобезопасных библиотеках. Здесь возвращаемый тип обобщённой функции зависит от типа её аргумента. Внутри функции нужно приведение as any, потому что TypeScript не может проверить тело функции по условному возвращаемому типу.

Prompt
Покажите обобщённую функцию, возвращаемый тип которой является условным типом, зависящим от её аргумента. Напишите processValue<T>(value: T), возвращающую string для строк, number для чисел, boolean для логических значений и never в остальных случаях; переводите строки в верхний регистр, удваивайте числа, инвертируйте логические значения (каждый раз с приведением as any) и выбрасывайте Unsupported type для всего остального. Вызовите её с "hello", 10 и true, сохранив результаты в stringResult, numberResult и boolResult с комментариями, указывающими "HELLO", 20 и false. Ничего не выводится.
Вывод: программа на TypeScript и захваченный вывод

Рекомендации

Делайте так:

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

Не делайте так:

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

Соображения производительности:

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