Пошук уроків, статей та іншого контенту
Застосуєте infer для вилучення типів елементів, параметрів функцій і результатів асинхронних операцій.
inferinfer — це спеціальне ключове слово TypeScript, яке дає змогу вивести та зберегти частину типу всередині умовного типу.
Загальний синтаксис:
type Result<T> = T extends SomePattern<infer U> ? U : never;Якщо тип T відповідає шаблону SomePattern<...>, TypeScript виводить тип, який стоїть на місці infer U, і повертає його.
Наприклад, можна отримати тип елементів масиву:
type ElementType<T> = T extends (infer U)[] ? U : never;
type Numbers = ElementType<number[]>; // number
type Names = ElementType<string[]>; // stringУ цьому прикладі:
TypeScript перевіряє, чи є T масивом.
infer U захоплює тип елемента масиву.
Якщо T — масив, повертається U.
Якщо ні — повертається never.
Створимо універсальний тип ArrayElement, який отримує тип елемента масиву:
type ArrayElement<T> = T extends readonly (infer Element)[]
? Element
: never;
type User = {
id: number;
name: string;
};
type UserElement = ArrayElement<User[]>;
// { id: number; name: string }
type ReadonlyUserElement = ArrayElement<readonly User[]>;
// { id: number; name: string }
type NotAnArray = ArrayElement<string>;
// neverreadonly у шаблоні дає змогу працювати як зі звичайними масивами, так і з масивами лише для читання.
infer можна застосовувати кілька разів:
type NestedElement<T> = T extends readonly (infer Element)[]
? Element extends readonly (infer InnerElement)[]
? InnerElement
: Element
: never;
type Result1 = NestedElement<string[]>;
// string
type Result2 = NestedElement<number[][]>;
// number
type Result3 = NestedElement<boolean[][][]>;
// boolean[] — вилучено лише один рівень вкладеностіОдин infer відповідає за один рівень вкладеності. Щоб отримати базовий тип із масиву будь-якої глибини, потрібен рекурсивний умовний тип:
type DeepArrayElement<T> = T extends readonly (infer Element)[]
? DeepArrayElement<Element>
: T;
type Value1 = DeepArrayElement<string[]>;
// string
type Value2 = DeepArrayElement<number[][][]>;
// number
type Value3 = DeepArrayElement<boolean>;
// booleanЗа допомогою infer можна отримати типи параметрів функції.
type FunctionParameters<T> = T extends (...args: infer Parameters)
? Parameters
: never;
type SaveUser = (name: string, age: number) => boolean;
type SaveUserParameters = FunctionParameters<SaveUser>;
// [name: string, age: number]Шаблон (...args: infer Parameters) означає:
тип має бути функцією;
її параметри захоплюються в тип Parameters;
результатом стає кортеж із типами параметрів.
Цей підхід лежить в основі вбудованого типу Parameters<T>:
type Log = (message: string, level: "info" | "error") => void;
type LogArguments = Parameters<Log>;
// [message: string, level: "info" | "error"]Оскільки результатом є кортеж, із нього можна отримати конкретний параметр:
type FirstParameter<T> = T extends (
first: infer First,
...rest: unknown[]
) => unknown
? First
: never;
type GetUser = (id: number, includePosts: boolean) => string;
type UserId = FirstParameter<GetUser>;
// numberДля отримання будь-якого параметра зручніше використати індексацію кортежу:
type SecondParameter<T> = T extends (...args: infer Parameters) => unknown
? Parameters[1]
: never;
type Search = (query: string, limit: number) => string[];
type Limit = SecondParameter<Search>;
// numberОднак такий тип поверне undefined, якщо функція не має другого параметра:
type OnlyOneArgument = (value: string) => void;
type MissingParameter = SecondParameter<OnlyOneArgument>;
// undefinedЩоб отримати тип, який повертає функція, використовується шаблон із infer після стрілки:
type FunctionReturn<T> = T extends (...args: unknown[]) => infer Result
? Result
: never;
type FormatUser = (userId: number) => string;
type FormattedUser = FunctionReturn<FormatUser>;
// stringДля цього також існує вбудований тип ReturnType<T>:
type Calculate = (left: number, right: number) => number;
type CalculationResult = ReturnType<Calculate>;
// number(...args: unknown[]) => unknownТакий шаблон описує функцію з будь-якими параметрами та будь-яким результатом:
type AnyFunction = (...args: unknown[]) => unknown;Під час роботи з infer цей шаблон дає змогу перевірити, що тип є функцією, і вилучити її результат.
У простих прикладах можна побачити (...args: any[]) => any, але unknown безпечніший, оскільки не вимикає перевірку типів так широко, як any.
PromiseАсинхронні функції повертають Promise. За допомогою infer можна вилучити тип значення, яке буде доступне після await:
type PromiseValue<T> = T extends Promise<infer Value>
? Value
: never;
type UserResponse = Promise<{ id: number; name: string }>;
type UserData = PromiseValue<UserResponse>;
// { id: number; name: string }
type TextResponse = PromiseValue<Promise<string>>;
// stringДля цього також є вбудований тип Awaited<T>:
type UserResult = Awaited<Promise<{ id: number; name: string }>>;
// { id: number; name: string }Awaited<T> працює не лише з Promise, а й рекурсивно обробляє вкладені проміси:
type NestedPromise = Promise<Promise<number>>;
type Value = Awaited<NestedPromise>;
// numberПоєднаємо вилучення результату функції та Promise:
type AsyncFunctionResult<T> = T extends (
...args: unknown[]
) => Promise<infer Result>
? Result
: never;
type LoadUser = (id: number) => Promise<{
id: number;
name: string;
}>;
type LoadedUser = AsyncFunctionResult<LoadUser>;
// { id: number; name: string }Спочатку TypeScript перевіряє, чи є тип функцією, яка повертає Promise. Потім infer Result вилучає тип усередині Promise.
Для довільної асинхронної функції можна спочатку отримати її результат, а потім застосувати Awaited:
type AsyncResult<T extends (...args: any[]) => any> = Awaited<ReturnType<T>>;
type FetchProducts = () => Promise<
Array<{
id: number;
title: string;
}>
>;
type Products = AsyncResult<FetchProducts>;
// { id: number; title: string }[]Такий варіант практичний, коли потрібно врахувати як звичайні, так і асинхронні функції.
infer може вилучати типи з об'єктів, якщо їхня структура відома.
type ArrayProperty<T> = T extends {
items: infer Item;
}
? Item
: never;
type ProductResponse = {
items: Array<{
id: number;
title: string;
}>;
};
type ProductsArray = ArrayProperty<ProductResponse>;
// { id: number; title: string }[]З цього типу можна додатково отримати тип одного елемента:
type ItemOfResponse<T> = T extends {
items: readonly (infer Item)[];
}
? Item
: never;
type Product = ItemOfResponse<ProductResponse>;
// { id: number; title: string }Ключ items у шаблоні є обов'язковим. Якщо об'єкт має іншу структуру, результатом буде never:
type InvalidResponse = ItemOfResponse<{
data: string[];
}>;
// neverinferВ одному умовному типі можна вилучати кілька типів одночасно:
type FunctionParts<T> = T extends (
...args: infer Parameters
) => infer Result
? {
parameters: Parameters;
result: Result;
}
: never;
type CreateUser = (
name: string,
age: number
) => {
id: number;
name: string;
};
type CreateUserParts = FunctionParts<CreateUser>;
/*
{
parameters: [name: string, age: number];
result: {
id: number;
name: string;
};
}
*/Це корисно, коли потрібно зберегти кілька характеристик типу в одному результаті.
Нижче наведено повний приклад, у якому:
вилучається тип параметрів функції;
вилучається результат асинхронної функції;
створюється обгортка, яка викликає функцію з правильними типами;
TypeScript перевіряє аргументи та результат.
type AsyncFunction<
Arguments extends unknown[],
Result
> = (...args: Arguments) => Promise<Result>;
type AsyncResult<T extends (...args: any[]) => any> =
Awaited<ReturnType<T>>;
async function withLogging<
T extends (...args: any[]) => any
>(
operation: T,
...args: Parameters<T>
): Promise<AsyncResult<T>> {
console.log("Початок операції");
const result = await operation(...args);
console.log("Операцію завершено");
return result as AsyncResult<T>;
}
type User = {
id: number;
name: string;
};
const loadUser: AsyncFunction<[id: number], User> = async (id) => {
return {
id,
name: `Користувач ${id}`,
};
};
async function main(): Promise<void> {
const user = await withLogging(loadUser, 42);
console.log(user.id);
console.log(user.name);
// Помилка TypeScript: аргумент має бути числом
// await withLogging(loadUser, "42");
}
main().catch((error: unknown) => {
console.error("Помилка:", error);
});Тип Parameters<T> зберігає параметри переданої функції, тому withLogging приймає саме ті аргументи, які потрібні конкретній операції.
Тип AsyncResult<T> використовує:
ReturnType<T> для отримання типу результату функції;
Awaited<T> для вилучення значення з Promise.
У результаті змінна user автоматично має тип:
{
id: number;
name: string;
}Якщо умовний тип працює з параметром-типом, який є об'єднанням, TypeScript застосовує його до кожного елемента об'єднання окремо:
type ElementType<T> = T extends readonly (infer Element)[]
? Element
: never;
type Values = ElementType<string[] | number[] | boolean[]>;
// string | number | booleanКожен тип із об'єднання перевіряється незалежно:
string[] перетворюється на string;
number[] перетворюється на number;
boolean[] перетворюється на boolean.
Тому результатом є об'єднання вилучених типів.
Іноді потрібно перевірити все об'єднання як одне ціле. Для цього тип обгортають у кортеж:
type IsArray<T> = [T] extends [readonly unknown[]]
? true
: false;
type OneArray = IsArray<string[]>;
// true
type ArrayOrString = IsArray<string[] | string>;
// falseБез квадратних дужок TypeScript перевіряв би кожен член об'єднання окремо. Квадратні дужки вимикають таку розподільчу поведінку.
inferinfer працює лише всередині умовного типу та лише під час зіставлення зі структурою:
type ExtractValue<T> = T extends { value: infer Value }
? Value
: never;Не можна використовувати infer як звичайну змінну типу:
// Неправильно
// type Value = infer T;Також результат може бути never, якщо переданий тип не відповідає шаблону:
type FunctionResult<T> = T extends (...args: unknown[]) => infer Result
? Result
: never;
type NotAFunction = FunctionResult<string>;
// neverТакий тип може несподівано не спрацювати для функцій із параметрами:
type Result<T> = T extends () => infer Value ? Value : never;Він відповідає лише функціям без параметрів. Для функції з аргументами потрібно описати параметри:
type Result<T> = T extends (...args: unknown[]) => infer Value
? Value
: never;Promise<T> і TТип результату асинхронної функції та тип значення після await — різні:
async function getCount(): Promise<number> {
return 10;
}
type FunctionResult = ReturnType<typeof getCount>;
// Promise<number>
type ResolvedResult = Awaited<ReturnType<typeof getCount>>;
// numberЯкщо потрібен тип значення після виконання промісу, використовуйте Awaited.
any без потребиany може приховати помилки:
type UnsafeResult<T> = T extends (...args: any[]) => any
? ReturnType<T>
: never;Для типів, які лише описують форму функції, краще використовувати unknown:
type SafeResult<T> = T extends (
...args: unknown[]
) => unknown
? ReturnType<T>
: never;Втім, вбудований ReturnType<T> має власне обмеження типу та є зручнішим вибором для більшості випадків.
neverЯкщо умовний тип має працювати лише з певною структурою, потрібно явно визначити результат для невідповідних типів:
type ElementType<T> = T extends readonly (infer Element)[]
? Element
: never;never означає, що з переданого типу нічого не можна вилучити.
inferinfer доречний, коли потрібно:
отримати тип елемента масиву;
отримати параметри функції;
отримати результат функції;
вилучити значення з Promise;
розібрати тип об'єкта за відомою структурою;
побудувати універсальний тип для обгортки або адаптера.
Якщо потрібна стандартна операція, спочатку перевірте вбудовані типи TypeScript:
Parameters<T> — параметри функції;
ReturnType<T> — тип результату функції;
InstanceType<T> — тип екземпляра конструктора;
Awaited<T> — значення після очікування промісу.
Власний infer варто створювати тоді, коли стандартних типів недостатньо або коли потрібно вилучити тип зі спеціальної структури.
infer дає змогу виводити частину типу всередині умовного типу.
Синтаксис infer Name створює типову змінну, яку TypeScript визначає зі структури типу.
За допомогою infer можна вилучати:
елементи масивів;
параметри функцій;
результати функцій;
значення з Promise;
типи властивостей об'єктів.
Parameters<T>, ReturnType<T> та Awaited<T> реалізують поширені варіанти таких операцій.
Для типів-об'єднань умовні типи зазвичай працюють розподільчо.
Якщо структура не відповідає шаблону, умовний тип зазвичай повертає never.