Български

Ръководство за '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 на условен тип, за да се декларира променлива на тип, която може да бъде изведена (inferred) от проверявания тип. По същество, тя ви позволява да „уловите“ част от даден тип за по-късна употреба.

Основен синтаксис:

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 и ги връща като tuple (кортеж).

2. Извличане на свойства от обектни типове

infer може да се използва и за извличане на специфични свойства от обектни типове, използвайки mapped types и условни типове:

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. Mapped type итерира през ключовете на T, а условният тип филтрира ключовете, които не съответстват на посочения тип.

3. Работа с Promises

Можете да извлечете типа, с който се разрешава (resolves) даден 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, който се очаква да бъде Promise. След това типът извежда разрешения тип U на Promise и го връща. Ако T не е promise, той връща T. Това е вграден помощен тип в по-новите версии на TypeScript.

4. Извличане на типа от масив от Promises

Комбинирането на Awaited и извличането на типове от масиви ви позволява да извлечете типа, с който се разрешава масив от Promises. Това е особено полезно при работа с 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 в масива, което води до tuple тип, където всеки елемент е awaited типът на съответния 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)

Да предположим, че имате функция за превод, която връща различни низове в зависимост от езиковата променлива (locale). Можете да използвате 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>> {
  // Симулация на 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 код, което в крайна сметка води до по-добър софтуер.