Menu

Условные типы и infer в TypeScript: примеры

Условный тип выбирает один из двух типов по результату проверки: T extends U ? X : Y. Синтаксис, как условные типы распределяются по объединениям (и как это отключить), извлечение типов через infer и как написать ReturnType самостоятельно.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Условный тип выбирает между двумя типами с помощью проверки, которая читается как тернарный оператор JavaScript: T extends U ? X : Y. Если T присваивается в U, результат X; иначе Y.

extends здесь означает «присваивается в», то же отношение, которое компилятор использует, когда вы присваиваете значение переменной. Условные типы существуют только при компиляции; из вывода JavaScript они стираются.

Синтаксис

type Result = CheckedType extends TestType ? TrueType : FalseType;

Условные типы становятся полезными с дженериками, где проверяемый тип это параметр типа, который позже получит конкретный тип. В ветке true TypeScript знает, что проверяемый тип прошёл проверку, поэтому T["message"] выше разрешён, хотя у простого T нет свойства message.

Они также вкладываются друг в друга, как цепочки тернарных операторов:

as TypeName<T> в теле функции не является необязательным, как показывает следующий раздел.

Условным типам возврата нужно утверждение

Функция, тип возврата которой это условный тип по её собственному параметру типа, не может вернуть ни одну ветку напрямую. TypeScript не сужает T внутри тела, поэтому не может определить, какая ветка действует:

index.ts(5,34): error TS2322: Type 'number' is not assignable to type 'Flip<T>'.
index.ts(5,45): error TS2322: Type 'string' is not assignable to type 'Flip<T>'.

Два обычных исправления: перегрузки, которые описывают каждую пару входа и выхода и точно проверяют вызывающий код, или утверждение в реализации.

Перегрузки описаны на странице о перегрузке функций. С утверждением компилятор верит вам на слово: неверная ветка в теле не будет замечена.

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

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

Распределение заставляет работать Exclude и Extract. Exclude<T, U> определён как T extends U ? never : T: каждый член, подходящий под U, становится never, а never исчезает из объединения. Поэтому Exclude<"a" | "b" | "c", "a"> это "b" | "c".

Из того же правила следуют два сюрприза. boolean это объединение true | false, поэтому ToArray<boolean> это false[] | true[], а не boolean[]. А never это пустое объединение, поэтому дистрибутивный условный тип, получивший never, возвращает never, ничего не проверяя:

type IsNever<T> = T extends never ? true : false;
type X = IsNever<never>; // never, not true

type IsNeverFixed<T> = [T] extends [never] ? true : false;
type Y = IsNeverFixed<never>; // true

Извлечение типов через infer

infer объявляет новую переменную типа внутри конструкции extends. Если сопоставление удалось, TypeScript заполняет эту переменную из проверяемого типа, и её можно использовать в ветке true:

Читайте T extends Promise<infer V> ? V : T как «если T это промис чего-то, назови это что-то V и верни его; иначе верни T без изменений». infer разрешён только в конструкции extends условного типа.

У переменной infer может быть собственное ограничение через extends. Тогда сопоставление удаётся, только если выведенный тип ему соответствует:

type FirstString<T> = T extends [infer S extends string, ...unknown[]] ? S : never;

type A = FirstString<["a", 1]>; // "a"
type B = FirstString<[1, "a"]>; // never: the first element is not a string

Собственный ReturnType

Встроенный ReturnType это однострочный условный тип с infer. Написать его самому это классическое упражнение, после которого обе идеи становятся понятны:

typeof makeUser превращает значение-функцию в её тип, затем условный тип сопоставляет его с «любой функцией» и захватывает тип возврата как R. Версия из стандартной библиотеки отличается двумя деталями: её параметр ограничен типами функций (T extends (...args: any) => any), поэтому ReturnType<string> это ошибка компиляции, а не never, и её ложная ветка это any. Страница о ReturnType описывает также Parameters, InstanceType и Awaited.

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

Условный тип может ссылаться на себя, что позволяет разворачивать вложенность любой глубины:

type Flatten<T> = T extends readonly (infer U)[] ? Flatten<U> : T;

type A = Flatten<number[][][]>; // number
type B = Flatten<string>;       // string

Встроенный Awaited<T> работает так же, разворачивая Promise<Promise<T>> до T. На практике держите рекурсию неглубокой: очень глубокая или неограниченная рекурсия заставляет компилятор сдаться с ошибкой error TS2589: Type instantiation is excessively deep and possibly infinite.

Краткий справочник

ШаблонЗначение
T extends U ? X : YX, если T присваивается в U, иначе Y
T extends U ? never : Tубрать члены, подходящие под U (это Exclude)
T extends U ? T : neverоставить члены, подходящие под U (это Extract)
[T] extends [U] ? X : Yта же проверка без распределения по объединению
T extends (infer E)[] ? E : Tтип элемента массива
T extends Promise<infer V> ? V : Tтип значения промиса
T extends (...args: any[]) => infer R ? R : neverтип возврата функции
T extends [infer H, ...infer Rest] ? ...первый элемент и остаток кортежа

Часто задаваемые вопросы

Что такое условный тип в TypeScript?

Тип вида T extends U ? X : Y. Если T присваивается в U, результат X, иначе Y. Это if/else для типов, вычисляемый при компиляции; в сгенерированном JavaScript от него ничего не остаётся.

Что делает ключевое слово infer в TypeScript?

infer объявляет переменную типа внутри конструкции extends условного типа и позволяет TypeScript заполнить её из сопоставленного типа. T extends Promise<infer V> ? V : T извлекает тип разрешённого значения из типа промиса. Использовать его можно только в конструкции extends условного типа.

Что такое дистрибутивный условный тип?

Когда проверяемый тип это голый параметр типа и вы передаёте объединение, условие применяется к каждому члену отдельно, а результаты объединяются. ToArray<string | number> при type ToArray<T> = T extends unknown ? T[] : never становится string[] | number[]. Чтобы это отключить, оберните обе стороны в квадратные скобки: [T] extends [unknown].

Как получить тип возврата функции в TypeScript?

Используйте встроенный ReturnType<typeof fn>. Это условный тип с infer: T extends (...args: any) => infer R ? R : any. Для асинхронных функций оберните его в Awaited<...>, чтобы получить разрешённое значение, а не промис.

Почему IsNever<never> возвращает never, а не true?

never это пустое объединение, а дистрибутивный условный тип перебирает члены объединения. Если членов нет, перебирать нечего, и результат never. Чтобы проверить на сам never, пишите [T] extends [never] ? true : false.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ