Українська

Вичерпний посібник з ключового слова '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 також можна використовувати для видобування конкретних властивостей з об'єктних типів за допомогою зіставлених та умовних типів:

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> {
  // Імітація виклику API
  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'); // Вивід: 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>> {
  // Імітація виклику API
  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, що в кінцевому підсумку призводить до кращого програмного забезпечення.