Если заметив ключевое слово infer в чужом TуpeScript-коде, вы делаете вид вам все понятно, но на самом деле — нет, вы не одиноки. До недавнего времени я и сам сыпался на интервью, когда речь заходила про эту возможность языка. Сегодня я покажу несколько примеров её использования, и постараюсь превратить ваше «Что у вас тут происходит?» по отношению к infer в «Ясненько-понятненько».
- Что такое infer? Самый скучный параграф
- Почему infer работает ТОЛЬКО в условных типах?
- Самый простой пример: Достаем тип массива
- Чуть более полезный пример. Достаем тип промиса
- Задачка со звездочкой. Вложенные промисы (рекурсия)
- Достаем тип функции. ReturnType своими руками
- Достаем аргументы функции
- Шпаргалка: Когда использовать infer
- Итог: что запомнить
Что такое infer? Самый скучный параграф
infer — это ключевое слово, которое используется только внутри условных типов (те самые conditional types, про которые мы уже говорили). Оно позволяет TypeScript извлечь (вывести) часть типа.
Сейчас будет страшно, но чуть ниже мы разберем. Синтаксис выглядит так:
type ExtractType<T> = T extends SomeType<infer U> ? U : never;
Переводим на человеческий:
- «TypeScript, посмотри на тип
T«. - «Если
Tрасширяет некийSomeType<...>, то попробуй вывести тип, который лежит внутри его угловых скобок, и назови егоU«. - «Если получилось — верни нам этот самый
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 |
| Достать тип из Promise | T 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 сам выведет правильные типы, что не может не радовать. Желаю успехов!







