Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Infer ключове слово в TypeScript — infer TypeScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**`infer`** - це ключове слово TypeScript, яке захоплює тип зсередини умовного типу при збігу. ```typescript type FnReturn<T> = T extends (...args: any[]) => infer R ? R : never; type Result = FnReturn<() => string>; // string type NotFn = FnReturn<number>; // never ``` **Ключове:** `infer R` прив'язується до будь-якого типу, що займає цю структурну позицію, коли збіг в `extends` відбувся. Захоплений тип доступний лише в true-гілці.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**`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-обгортки, і типи починали розходитись між членами команди щоразу як їх писали вручну.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.