Полное руководство по ключевому слову 'infer' в TypeScript. Объяснение его использования с условными типами для извлечения и манипуляции типами, включая продвинутые сценарии.
Освоение TypeScript Infer: Извлечение типов с помощью условных типов для расширенных манипуляций
Система типов TypeScript невероятно мощна, позволяя разработчикам создавать надежные и поддерживаемые приложения. Одной из ключевых особенностей, обеспечивающих эту мощь, является ключевое слово infer
, используемое в сочетании с условными типами. Эта комбинация предоставляет механизм для извлечения конкретных типов из сложных структур. В этой статье мы подробно рассмотрим ключевое слово infer
, объясним его функциональность и продемонстрируем продвинутые сценарии использования. Мы изучим практические примеры, применимые к различным сценариям разработки программного обеспечения, от взаимодействия с API до манипулирования сложными структурами данных.
Что такое условные типы?
Прежде чем мы погрузимся в infer
, давайте кратко рассмотрим условные типы. Условные типы в TypeScript позволяют определять тип на основе условия, подобно тернарному оператору в JavaScript. Базовый синтаксис выглядит так:
T extends U ? X : Y
Это читается так: «Если тип T
может быть присвоен типу U
, то используется тип X
; в противном случае — тип Y
».
Пример:
type IsString<T> = T extends string ? true : false;
type StringResult = IsString<string>; // type StringResult = true
type NumberResult = IsString<number>; // type NumberResult = false
Представляем ключевое слово infer
Ключевое слово infer
используется в предложении extends
условного типа для объявления переменной типа, которая может быть выведена из проверяемого типа. По сути, оно позволяет «захватить» часть типа для последующего использования.
Базовый синтаксис:
type MyType<T> = T extends (infer U) ? U : never;
В этом примере, если T
может быть присвоен какому-либо типу, TypeScript попытается вывести тип U
. Если вывод успешен, итоговым типом будет U
; в противном случае — never
.
Простые примеры использования infer
1. Вывод возвращаемого типа функции
Частым случаем использования является вывод возвращаемого типа функции:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
function add(a: number, b: number): number {
return a + b;
}
type AddReturnType = ReturnType<typeof add>; // type AddReturnType = number
function greet(name: string): string {
return `Hello, ${name}!`;
}
type GreetReturnType = ReturnType<typeof greet>; // type GreetReturnType = string
В этом примере ReturnType<T>
принимает на вход тип функции T
. Он проверяет, можно ли присвоить T
функции, которая принимает любые аргументы и возвращает значение. Если да, он выводит возвращаемый тип как R
и возвращает его. В противном случае он возвращает any
.
2. Вывод типа элемента массива
Другой полезный сценарий — извлечение типа элемента из массива:
type ArrayElementType<T> = T extends (infer U)[] ? U : never;
type NumberArrayType = ArrayElementType<number[]>; // type NumberArrayType = number
type StringArrayType = ArrayElementType<string[]>; // type StringArrayType = string
type MixedArrayType = ArrayElementType<(string | number)[]>; // type MixedArrayType = string | number
type NotAnArrayType = ArrayElementType<number>; // type NotAnArrayType = never
Здесь ArrayElementType<T>
проверяет, является ли T
типом массива. Если да, он выводит тип элемента как U
и возвращает его. Если нет, он возвращает never
.
Продвинутые сценарии использования infer
1. Вывод параметров конструктора
Вы можете использовать infer
для извлечения типов параметров функции-конструктора:
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;
class Person {
constructor(public name: string, public age: number) {}
}
type PersonConstructorParams = ConstructorParameters<typeof Person>; // type PersonConstructorParams = [string, number]
class Point {
constructor(public x: number, public y: number) {}
}
type PointConstructorParams = ConstructorParameters<typeof Point>; // type PointConstructorParams = [number, number]
В этом случае ConstructorParameters<T>
принимает тип функции-конструктора T
. Он выводит типы параметров конструктора как P
и возвращает их в виде кортежа.
2. Извлечение свойств из объектных типов
infer
также можно использовать для извлечения определенных свойств из объектных типов с помощью сопоставленных (mapped) и условных типов:
type PickByType<T, K extends keyof T, U> = {
[P in K as T[P] extends U ? P : never]: T[P];
};
interface User {
id: number;
name: string;
age: number;
email: string;
isActive: boolean;
}
type StringProperties = PickByType<User, keyof User, string>; // type StringProperties = { name: string; email: string; }
type NumberProperties = PickByType<User, keyof User, number>; // type NumberProperties = { id: number; age: number; }
//Интерфейс, представляющий географические координаты.
interface GeoCoordinates {
latitude: number;
longitude: number;
altitude: number;
country: string;
city: string;
timezone: string;
}
type NumberCoordinateProperties = PickByType<GeoCoordinates, keyof GeoCoordinates, number>; // type NumberCoordinateProperties = { latitude: number; longitude: number; altitude: number; }
Здесь PickByType<T, K, U>
создает новый тип, который включает только те свойства из T
(с ключами из K
), значения которых могут быть присвоены типу U
. Сопоставленный тип итерирует по ключам T
, а условный тип отфильтровывает ключи, не соответствующие указанному типу.
3. Работа с промисами (Promises)
Вы можете вывести тип, с которым разрешается Promise
:
type Awaited<T> = T extends Promise<infer U> ? U : T;
async function fetchData(): Promise<string> {
return 'Data from API';
}
type FetchDataType = Awaited<ReturnType<typeof fetchData>>; // type FetchDataType = string
async function fetchNumbers(): Promise<number[]> {
return [1, 2, 3];
}
type FetchedNumbersType = Awaited<ReturnType<typeof fetchNumbers>>; //type FetchedNumbersType = number[]
Тип Awaited<T>
принимает тип T
, который, как ожидается, является промисом. Затем тип выводит разрешенный тип U
промиса и возвращает его. Если T
не является промисом, он возвращает T. Это встроенный служебный тип в новых версиях TypeScript.
4. Извлечение типа из массива промисов
Комбинирование Awaited
и вывода типа массива позволяет вывести тип, с которым разрешается массив промисов. Это особенно полезно при работе с Promise.all
.
type PromiseArrayReturnType<T extends Promise<any>[]> = {
[K in keyof T]: Awaited<T[K]>;
};
async function getUSDRate(): Promise<number> {
return 0.0069;
}
async function getEURRate(): Promise<number> {
return 0.0064;
}
const rates = [getUSDRate(), getEURRate()];
type RatesType = PromiseArrayReturnType<typeof rates>;
// type RatesType = [number, number]
В этом примере сначала определяются две асинхронные функции, getUSDRate
и getEURRate
, которые имитируют получение обменных курсов. Затем служебный тип PromiseArrayReturnType
извлекает разрешенный тип из каждого Promise
в массиве, в результате чего получается тип-кортеж, где каждый элемент является ожидаемым типом соответствующего промиса.
Практические примеры в различных областях
1. Приложение для электронной коммерции
Рассмотрим приложение для электронной коммерции, где вы получаете детали продукта из API. Вы можете использовать infer
для извлечения типа данных продукта:
interface Product {
id: number;
name: string;
price: number;
description: string;
imageUrl: string;
category: string;
rating: number;
countryOfOrigin: string;
}
async function fetchProduct(productId: number): Promise<Product> {
// Simulate API call
return new Promise((resolve) => {
setTimeout(() => {
resolve({
id: productId,
name: 'Example Product',
price: 29.99,
description: 'A sample product',
imageUrl: 'https://example.com/image.jpg',
category: 'Electronics',
rating: 4.5,
countryOfOrigin: 'Canada'
});
}, 500);
});
}
type ProductType = Awaited<ReturnType<typeof fetchProduct>>; // type ProductType = Product
function displayProductDetails(product: ProductType) {
console.log(`Product Name: ${product.name}`);
console.log(`Price: ${product.price} ${product.countryOfOrigin === 'Canada' ? 'CAD' : (product.countryOfOrigin === 'USA' ? 'USD' : 'EUR')}`);
}
fetchProduct(123).then(displayProductDetails);
В этом примере мы определяем интерфейс Product
и функцию fetchProduct
, которая получает детали продукта из API. Мы используем Awaited
и ReturnType
для извлечения типа Product
из возвращаемого типа функции fetchProduct
, что позволяет нам проверять типы в функции displayProductDetails
.
2. Интернационализация (i18n)
Предположим, у вас есть функция перевода, которая возвращает разные строки в зависимости от локали. Вы можете использовать infer
для извлечения возвращаемого типа этой функции для обеспечения безопасности типов:
interface Translations {
greeting: string;
farewell: string;
welcomeMessage: (name: string) => string;
}
const enTranslations: Translations = {
greeting: 'Hello',
farewell: 'Goodbye',
welcomeMessage: (name: string) => `Welcome, ${name}!`,
};
const frTranslations: Translations = {
greeting: 'Bonjour',
farewell: 'Au revoir',
welcomeMessage: (name: string) => `Bienvenue, ${name}!`,
};
function getTranslation(locale: 'en' | 'fr'): Translations {
return locale === 'en' ? enTranslations : frTranslations;
}
type TranslationType = ReturnType<typeof getTranslation>;
function greetUser(locale: 'en' | 'fr', name: string) {
const translations = getTranslation(locale);
console.log(translations.welcomeMessage(name));
}
greetUser('fr', 'Jean'); // Output: Bienvenue, Jean!
Здесь TranslationType
выводится как интерфейс Translations
, что гарантирует, что функция greetUser
имеет правильную информацию о типах для доступа к переведенным строкам.
3. Обработка ответов API
При работе с API структура ответа может быть сложной. infer
может помочь извлечь определенные типы данных из вложенных ответов API:
interface ApiResponse<T> {
status: number;
data: T;
message?: string;
}
interface UserData {
id: number;
username: string;
email: string;
profile: {
firstName: string;
lastName: string;
country: string;
language: string;
}
}
async function fetchUser(userId: number): Promise<ApiResponse<UserData>> {
// Simulate API call
return new Promise((resolve) => {
setTimeout(() => {
resolve({
status: 200,
data: {
id: userId,
username: 'johndoe',
email: 'john.doe@example.com',
profile: {
firstName: 'John',
lastName: 'Doe',
country: 'USA',
language: 'en'
}
}
});
}, 500);
});
}
type UserApiResponse = Awaited<ReturnType<typeof fetchUser>>;
type UserProfileType = UserApiResponse['data']['profile'];
function displayUserProfile(profile: UserProfileType) {
console.log(`Name: ${profile.firstName} ${profile.lastName}`);
console.log(`Country: ${profile.country}`);
}
fetchUser(123).then((response) => {
if (response.status === 200) {
displayUserProfile(response.data.profile);
}
});
В этом примере мы определяем интерфейс ApiResponse
и интерфейс UserData
. Мы используем infer
и индексацию типов для извлечения UserProfileType
из ответа API, гарантируя, что функция displayUserProfile
получает правильный тип.
Лучшие практики использования infer
- Будьте проще: Используйте
infer
только при необходимости. Чрезмерное использование может усложнить чтение и понимание вашего кода. - Документируйте свои типы: Добавляйте комментарии, чтобы объяснить, что делают ваши условные типы и выражения с
infer
. - Тестируйте свои типы: Используйте проверку типов TypeScript, чтобы убедиться, что ваши типы ведут себя как ожидается.
- Учитывайте производительность: Сложные условные типы иногда могут влиять на время компиляции. Помните о сложности ваших типов.
- Используйте служебные типы: TypeScript предоставляет несколько встроенных служебных типов (например,
ReturnType
,Awaited
), которые могут упростить ваш код и уменьшить потребность в пользовательских выражениях сinfer
.
Распространенные ошибки
- Неправильный вывод типа: Иногда TypeScript может вывести тип, который не соответствует вашим ожиданиям. Дважды проверьте определения типов и условия.
- Циклические зависимости: Будьте осторожны при определении рекурсивных типов с использованием
infer
, так как это может привести к циклическим зависимостям и ошибкам компиляции. - Чрезмерно сложные типы: Избегайте создания слишком сложных условных типов, которые трудно понять и поддерживать. Разбивайте их на более мелкие и управляемые типы.
Альтернативы infer
Хотя infer
является мощным инструментом, существуют ситуации, когда другие подходы могут быть более уместными:
- Утверждения типа (Type Assertions): В некоторых случаях вы можете использовать утверждения типа, чтобы явно указать тип значения вместо его вывода. Однако будьте осторожны с утверждениями типа, так как они могут обходить проверку типов.
- Защитники типа (Type Guards): Защитники типа можно использовать для сужения типа значения на основе проверок во время выполнения. Это полезно, когда вам нужно обрабатывать разные типы в зависимости от условий времени выполнения.
- Служебные типы: TypeScript предоставляет богатый набор служебных типов, которые могут справиться со многими общими задачами по манипуляции типами без необходимости в пользовательских выражениях с
infer
.
Заключение
Ключевое слово infer
в TypeScript в сочетании с условными типами открывает расширенные возможности для манипуляции типами. Оно позволяет извлекать определенные типы из сложных структур, что дает возможность писать более надежный, поддерживаемый и типобезопасный код. От вывода возвращаемых типов функций до извлечения свойств из объектных типов — возможности огромны. Понимая принципы и лучшие практики, изложенные в этом руководстве, вы сможете использовать infer
в полной мере и повысить свои навыки в TypeScript. Не забывайте документировать свои типы, тщательно их тестировать и рассматривать альтернативные подходы, когда это уместно. Освоение infer
позволяет вам писать действительно выразительный и мощный код на TypeScript, что в конечном итоге приводит к созданию более качественного программного обеспечения.