Skip to main content

Infer ключове слово в TypeScript — infer TypeScript

infer - це ключове слово TypeScript, яке захоплює та іменує тип зсередини збігу умовного типу, дозволяючи витягувати частини структури типу без попереднього знання того, що там знаходиться.

Теорія

Коротко

  • infer схожий на групу захоплення в регулярних виразах: описуєш форму, TypeScript підставляє відповідний тип
  • Працює лише всередині клаузи extends умовного типу
  • Захоплений тип доступний тільки в true-гілці
  • Використовуй, коли вкладений тип повторюється в кількох місцях API і хочеш витягнути його один раз
  • Пропускай, якщо тип вже відомий: використовуй індексний доступ T['key']

Базовий приклад

typescript
// Витягуємо тип повернення будь-якої функції 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>; // never

infer 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 поза умовним типом

typescript
// infer існує тільки всередині клаузи extends type Bad<T> = infer R; // Error: 'infer' declarations are only permitted in the 'extends' clause

infer не є самостійним ключовим словом. Поза клаузою extends умовного типу воно позбавлене сенсу.

Помилка 2: Очікування захопленого типу у false-гілці

typescript
type Foo<T> = T extends { x: infer U } ? U : U; // Error - U не в scope тут

U доступний лише в true-гілці. Як тільки збіг не відбувся і TypeScript пішов по false-шляху, захоплена змінна зникає.

Помилка 3: Ігнорування розподілу (distribution) по union-типах

typescript
type ElementType<T> = T extends (infer U)[] ? U : never; type Result = ElementType<string[] | number[]>; // string | number

TypeScript розподіляє умовні типи по union, застосовуючи умову до кожного члена окремо і потім об'єднуючи результати. Зазвичай це саме те, що потрібно, але може здивувати, якщо один член union не відповідає патерну.

Помилка 4: Захоплення параметрів функції по позиції замість rest

typescript
// Крихко: підходить тільки для функцій рівно з двома параметрами 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.

Приклади

Витягування типу елемента масиву

typescript
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>; // never

readonly в патерні важливий. Без нього передача readonly string[] давала б never, бо тип не збігається зі звичайним мутабельним масивом. Додавання readonly робить патерн сумісним і зі звичайними масивами, і з readonly-кортежами.

Витягування параметрів функції

typescript
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

typescript
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-обгортки, і типи починали розходитись між членами команди щоразу як їх писали вручну.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?