Русский

Полное руководство по ключевому слову '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 является мощным инструментом, существуют ситуации, когда другие подходы могут быть более уместными:

Заключение

Ключевое слово infer в TypeScript в сочетании с условными типами открывает расширенные возможности для манипуляции типами. Оно позволяет извлекать определенные типы из сложных структур, что дает возможность писать более надежный, поддерживаемый и типобезопасный код. От вывода возвращаемых типов функций до извлечения свойств из объектных типов — возможности огромны. Понимая принципы и лучшие практики, изложенные в этом руководстве, вы сможете использовать infer в полной мере и повысить свои навыки в TypeScript. Не забывайте документировать свои типы, тщательно их тестировать и рассматривать альтернативные подходы, когда это уместно. Освоение infer позволяет вам писать действительно выразительный и мощный код на TypeScript, что в конечном итоге приводит к созданию более качественного программного обеспечения.