Infer ключове слово в TypeScript — infer TypeScript
infer - це ключове слово TypeScript, яке захоплює та іменує тип зсередини збігу умовного типу, дозволяючи витягувати частини структури типу без попереднього знання того, що там знаходиться.
Теорія
Коротко
inferсхожий на групу захоплення в регулярних виразах: описуєш форму, TypeScript підставляє відповідний тип- Працює лише всередині клаузи
extendsумовного типу - Захоплений тип доступний тільки в true-гілці
- Використовуй, коли вкладений тип повторюється в кількох місцях API і хочеш витягнути його один раз
- Пропускай, якщо тип вже відомий: використовуй індексний доступ
T['key']
Базовий приклад
// Витягуємо тип повернення будь-якої функції
type FnReturn<T> = T extends (...args: any[]) => infer R ? R : never;
type AddReturn = FnReturn<(a: number, b: number) => number>; // number
type StringReturn = FnReturn<() => string>; // string
type NotAFn = FnReturn<string>; // neverinfer R каже TypeScript: "що б не стояло на позиції повернення, захопи це як R." Якщо збіг вдався, R стає типом повернення. Якщо ні, false-гілка повертає never.
Чим infer відрізняється від прямого доступу до типу
Прямий доступ на кшталт T['returnType'] працює, коли ім'я властивості відоме. infer працює, коли відома структура, але не внутрішній тип. Ти можеш зробити збіг по сигнатурі функції, масиву чи Promise і витягнути тип, що знаходиться всередині, незалежно від того, чим він виявиться. В цьому вся суть: infer захоплює типи зі структурних позицій, а не за іменованими ключами.
Без infer витягування типу повернення функції вимагало б або хардкодингу типу, або передачі його як окремого generic-параметра. Обидва варіанти ламаються, коли функція надходить ззовні.
Коли використовувати
- Витягти тип повернення функції:
T extends (...args: any[]) => infer R ? R : never - Отримати тип елемента масиву:
T extends readonly (infer U)[] ? U : never - Розгорнути Promise:
T extends Promise<infer U> ? U : never - Захопити параметри функції як кортеж:
T extends (...args: infer A) => any ? A : never - Уникай
infer, якщо тип вже відомий: використовуйT[0]для відомої позиції кортежу,T['value']для відомої властивості
Як компілятор обробляє infer
TypeScript розв'язує infer під час перевірки типів, а не під час виконання. Коли компілятор обчислює умовний тип, він намагається зіставити вхідний тип з патерном в extends. Якщо збіг відбувся, infer R прив'язується до типу, що займає цю структурну позицію, і R підставляється в true-гілку. Все це відбувається на етапі компіляції в type mapper, аналогічно до того, як інстанціюються generic-параметри. Жодного коду не генерується, жодного навантаження під час виконання. Прив'язана змінна існує лише в true-гілці цього конкретного умовного типу.
Типові помилки
Помилка 1: Використання infer поза умовним типом
// infer існує тільки всередині клаузи extends
type Bad<T> = infer R; // Error: 'infer' declarations are only permitted in the 'extends' clauseinfer не є самостійним ключовим словом. Поза клаузою extends умовного типу воно позбавлене сенсу.
Помилка 2: Очікування захопленого типу у false-гілці
type Foo<T> = T extends { x: infer U } ? U : U; // Error - U не в scope тутU доступний лише в true-гілці. Як тільки збіг не відбувся і TypeScript пішов по false-шляху, захоплена змінна зникає.
Помилка 3: Ігнорування розподілу (distribution) по union-типах
type ElementType<T> = T extends (infer U)[] ? U : never;
type Result = ElementType<string[] | number[]>; // string | numberTypeScript розподіляє умовні типи по union, застосовуючи умову до кожного члена окремо і потім об'єднуючи результати. Зазвичай це саме те, що потрібно, але може здивувати, якщо один член union не відповідає патерну.
Помилка 4: Захоплення параметрів функції по позиції замість rest
// Крихко: підходить тільки для функцій рівно з двома параметрами
type TwoArgReturn<T> = T extends (a: infer A, b: infer B) => infer R ? R : never;
// Краще: захоплює всі параметри як кортеж незалежно від їх кількості
type Params<T> = T extends (...args: infer A) => any ? A : never;Використання ...args: infer A дає тебе кортеж всіх параметрів за один раз. Збіг по позиціях працює лише якщо функція має фіксовану, заздалегідь відому сигнатуру.
Помилка 5: Очікування що infer щось робить під час виконання
infer повністю стирається при компіляції. Якщо потрібно перевіряти або інспектувати типи під час виконання, потрібен інший інструмент: typeof, type guards або бібліотека схем на кшталт Zod.
Де зустрічається в реальних проєктах
- Вбудовані утиліти:
ReturnType<T>,Parameters<T>іAwaited<T>- всі побудовані наinferв стандартній бібліотеці TypeScript - React/Redux:
ReturnType<typeof useSelector>витягує тип повернення селектора, даючи тип зрізу стору без повторного написання - Zod:
z.infer<typeof schema>отримує TypeScript-тип зі схеми валідації (визначення типу (inference) зі схеми) - tRPC:
inferRouterOutputs<AppRouter>дає типізовані відповіді API, похідні безпосередньо від роутера - TanStack Query: hook generics використовують патерни на основі
inferдля передачі типу даних через весь pipeline запиту
Питання на співбесіді
Q: Напиши тип, який витягує кортеж аргументів будь-якої функції.
A: type Params<T> = T extends (...args: infer A) => any ? A : never; Синтаксис rest ...args захоплює всі параметри як єдиний типізований кортеж, незалежно від їх кількості.
Q: Що повертає false-гілка, якщо збіг infer не відбувся?
A: Те, що ти вказав явно, зазвичай never. Змінна infer просто не існує у false-гілці. Немає нічого, до чого можна було б відкотитись.
Q: Як infer поводиться, коли вхідний тип є union?
A: TypeScript розподіляє умовний тип по кожному члену union окремо. ElementType<string[] | number[]> обчислює ElementType<string[]> і ElementType<number[]> незалежно, потім об'єднує результати в string | number.
Q: Чи можна використовувати infer для рекурсивного розгортання вкладених Promise?
A: Так. type Unwrap<T> = T extends Promise<infer U> ? Unwrap<U> : T; продовжує розгортати до тих пір, поки найглибший тип не перестане бути Promise. Рекурсивні умовні типи потребують TypeScript 4.1 або новіший.
Q: Яка різниця між infer і mapped types?
A: Mapped types ітерують по відомих ключах і трансформують їх. infer робить збіг по структурному патерну, щоб захопити невідомий внутрішній тип. Вони вирішують зовсім різні задачі і часто використовуються разом в складних утилітарних типах.
Q: (Senior) Чи може infer захоплювати типи з intersection так само, як з union?
A: Ні. Intersection-типи не розподіляються як union. T extends A & B з infer всередині B не дає ізольованого доступу до того, що привносить A. Зазвичай потрібні окремі умовні типи для кожної частини з подальшим ручним об'єднанням результатів через intersection.
Приклади
Витягування типу елемента масиву
type ElementType<T> = T extends readonly (infer U)[] ? U : never;
type NumArr = ElementType<number[]>; // number
type Mixed = ElementType<readonly [string, boolean]>; // string | boolean
type NotArray = ElementType<string>; // neverreadonly в патерні важливий. Без нього передача readonly string[] давала б never, бо тип не збігається зі звичайним мутабельним масивом. Додавання readonly робить патерн сумісним і зі звичайними масивами, і з readonly-кортежами.
Витягування параметрів функції
type HookParams<T> = T extends (...args: infer A) => any ? A : never;
declare function useDebounce(value: string, delay: number): string;
type DebounceParams = HookParams<typeof useDebounce>;
// [value: string, delay: number]
// Обгортаємо будь-яку функцію без повторення її сигнатури:
function withLogging<T extends (...args: any[]) => any>(
fn: T,
...args: HookParams<T>
): ReturnType<T> {
console.log("calling with", args);
return fn(...args);
}Цей патерн особливо корисний при обгортанні сторонніх хуків або middleware. Якщо useDebounce змінить сигнатуру після оновлення бібліотеки, HookParams підхопить це автоматично без жодних ручних правок типів.
Рекурсивне розгортання Promise
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
type A = DeepAwaited<Promise<string>>; // string
type B = DeepAwaited<Promise<Promise<number>>>; // number
type C = DeepAwaited<Promise<Promise<() => boolean>>>; // () => boolean
type D = DeepAwaited<string>; // string (повертається як є)Саме так влаштований вбудований утилітарний тип Awaited<T> в TypeScript. Рекурсивний виклик продовжує розгортати до тих пір, поки найглибший тип не перестане бути Promise. Цей патерн я використовував на проєкті де типи відповідей API були вкладені на два-три рівні вглибину через різні service-обгортки, і типи починали розходитись між членами команди щоразу як їх писали вручну.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.