Ключевое слово infer в TypeScript. Распаковка типов на примерах

Frontend

Если заметив ключевое слово infer в чужом TуpeScript-коде, вы делаете вид вам все понятно, но на самом деле — нет, вы не одиноки. До недавнего времени я и сам сыпался на интервью, когда речь заходила про эту возможность языка. Сегодня я покажу несколько примеров её использования, и постараюсь превратить ваше «Что у вас тут происходит?» по отношению к infer в «Ясненько-понятненько».

Что такое infer? Самый скучный параграф

infer — это ключевое слово, которое используется только внутри условных типов (те самые conditional types, про которые мы уже говорили). Оно позволяет TypeScript извлечь (вывести) часть типа.

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

type ExtractType<T> = T extends SomeType<infer U> ? U : never;

Переводим на человеческий:

  1. «TypeScript, посмотри на тип T«.
  2. «Если T расширяет некий SomeType<...>, то попробуй вывести тип, который лежит внутри его угловых скобок, и назови его U«.
  3. «Если получилось — верни нам этот самый U. Если нет — отдай never.

Почему infer работает ТОЛЬКО в условных типах?

Важно запомнить: infer нельзя использовать где попало. Он сработает только в правом от extends типе, внутри условного типа.

Неправильно:

type Wrong<T> = infer T; // Ошибка! infer только внутри extends

Правильно:

type Correct<T> = T extends SomeType<infer U> ? U : never;

TypeScript должен иметь «контекст» для вывода. Условный тип дает этот контекст: «Если T расширяет некий тип, то выведи U».

Зато мы можем вывести сразу несколько типов:

type ExtractFromObject<T> = T extends { a: infer A; b: infer B } ? [A, B] : never;

type Result = ExtractFromObject<{ a: string; b: number }>; // [string, number]

Самый простой пример: Достаем тип массива

Итак, переходим к обещанным примерам. Допустим, у вас есть массив, но вы не знаете, что в нем лежит. Вы хотите получить тип его элементов.

type ElementType<T> = T extends (infer U)[] ? U : never;

type A = ElementType<string[]>; // string
type B = ElementType<number[]>; // number
type C = ElementType<boolean>; // never (потому что boolean не массив)

Что здесь произошло? Попробуем еще раз перевести на «человеческий»:

  • T extends (infer U)[] — мы говорим: «Если T — это массив (любой), то попробуй вывести тип элемента и назови его U«.
  • TypeScript видит string[], подставляет вместо U — string, и возвращает string.
  • Для number[] возвращает number.

Мы не знали тип заранее, но TypeScript сам его вывел.

Чуть более полезный пример. Достаем тип промиса

Теперь вы работаете с асинхронным кодом, и у вас есть Promise<T>. Вы хотите получить T.

type Awaited<T> = T extends Promise<infer U> ? U : never;

type Result1 = Awaited<Promise<string>>;   // string
type Result2 = Awaited<Promise<number>>;   // number
type Result3 = Awaited<number>;            // never

И да, начиная с TypeScript 4.5, Awaited уже встроен в язык. Но иногда полезно написать свою версию, чтобы понять механику работы нативных ютилити-тайпов.

Задачка со звездочкой. Вложенные промисы (рекурсия)

А что если у вас промис, который возвращает другой промис? И да, опять нужно узнать тип возвращаемого значения. Можно сделать рекурсивный Awaited:

type DeepAwaited<T> = T extends Promise<infer U> 
    ? DeepAwaited<U>  // рекурсивно достаем дальше
    : T;
    
type Result = DeepAwaited<Promise<Promise<Promise<string>>>>; // string

Теперь TypeScript рекурсивно «разворачивает» промисы, пока не найдет не-промис и тогда вернет нам тип его значения.

Достаем тип функции. ReturnType своими руками

Уверен, вы знаете встроенный утилити-тайп ReturnType<T>. Он достает тип возвращаемого значения функции. Вот как можно самостоятельно реализовать ее при помощи infer:

type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

const sayHello = () => {
    return 'Hello';
}

const add = (a: number, b: number) => {
    return a + b;
}

type Hello = MyReturnType<typeof sayHello>; // string
type Sum = MyReturnType<typeof add>;        // number

Здесь при помощи infer R мы говорим: «Посмотри на возвращаемый тип функции и назови его R«.

Достаем аргументы функции

Если мы можем достать тип возвращаемого значения, то аналогичным методом можем достать и типы аргументов:

type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
const greet = (name: string, age: number) => {}
type Args = MyParameters<typeof greet>; // [string, number]

Теперь у нас есть кортеж из типов аргументов.

Шпаргалка: Когда использовать infer

ЗадачаПример использования infer
Достать тип элемента массиваT extends (infer U)[] ? U : never
Достать тип из PromiseT extends Promise<infer U> ? U : never
Достать возвращаемый тип функцииT extends (...args: any[]) => infer R ? R : never
Достать аргументы функцииT extends (...args: infer P) => any ? P : never
Достать тип свойства объектаT extends { key: infer V } ? V : never
Распаковать вложенные типыРекурсивный вызов с infer
Вывести несколько типов одновременно{ a: infer A; b: infer B }

Итог: что запомнить

  • infer — это инструкция TypeScript: «Выведи тип сам, я не знаю, что там»
  • Используется только внутри условных типов (T extends ... ? ... : ...).
  • Позволяет «распаковывать» сложные типы: массивы, промисы, функции, объекты.

Теперь, когда вы увидите в чьем-то реквесте infer, вы скажете: «Ага, тут достают тип из промиса / массива / функции».

Еще одна неочевидная вещь: чем лучше вы понимаете infer, тем меньше потребности в any в вашем коде. TypeScript сам выведет правильные типы, что не может не радовать. Желаю успехов!

Симо Мофин
Симо Мофин

Senior Frontend Developer
Главный по блогу