Čeština

Komplexní průvodce klíčovým slovem 'infer' v TypeScriptu, vysvětlující jeho použití s podmíněnými typy pro efektivní extrakci a manipulaci s typy, včetně pokročilých případů použití.

Zvládnutí TypeScript Infer: Extrakce Podmíněného Typu pro Pokročilou Manipulaci s Typy

Systém typů v TypeScriptu je neuvěřitelně výkonný a umožňuje vývojářům vytvářet robustní a udržovatelné aplikace. Jednou z klíčových funkcí, která tuto sílu umožňuje, je klíčové slovo infer používané ve spojení s podmíněnými typy. Tato kombinace poskytuje mechanismus pro extrakci specifických typů z komplexních typových struktur. Tento příspěvek na blogu se hlouběji zaměřuje na klíčové slovo infer, vysvětluje jeho funkčnost a představuje pokročilé případy použití. Prozkoumáme praktické příklady použitelné v různých scénářích vývoje softwaru, od interakce s API až po manipulaci s komplexními datovými strukturami.

Co jsou podmíněné typy?

Než se ponoříme do infer, pojďme si rychle zopakovat podmíněné typy. Podmíněné typy v TypeScriptu vám umožňují definovat typ na základě podmínky, podobně jako ternární operátor v JavaScriptu. Základní syntaxe je:

T extends U ? X : Y

To se čte jako: „Pokud je typ T přiřaditelný typu U, pak je typ X; jinak je typ Y.“

Příklad:

type IsString<T> = T extends string ? true : false;

type StringResult = IsString<string>; // type StringResult = true
type NumberResult = IsString<number>; // type NumberResult = false

Představujeme klíčové slovo infer

Klíčové slovo infer se používá v rámci klauzule extends podmíněného typu k deklaraci proměnné typu, která může být odvozena z typu, který se kontroluje. V podstatě vám umožňuje „zachytit“ část typu pro pozdější použití.

Základní syntaxe:

type MyType<T> = T extends (infer U) ? U : never;

V tomto příkladu, pokud je T přiřaditelný k nějakému typu, TypeScript se pokusí odvodit typ U. Pokud je odvození úspěšné, typ bude U; jinak to bude never.

Jednoduché příklady infer

1. Odvození návratového typu funkce

Běžným případem použití je odvození návratového typu funkce:

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

V tomto příkladu bere ReturnType<T> jako vstup typ funkce T. Zkontroluje, zda je T přiřaditelný funkci, která přijímá libovolné argumenty a vrací hodnotu. Pokud ano, odvodí návratový typ jako R a vrátí jej. Jinak vrátí any.

2. Odvození typu prvku pole

Dalším užitečným scénářem je extrakce typu prvku z pole:

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

Zde ArrayElementType<T> kontroluje, zda je T typ pole. Pokud ano, odvodí typ prvku jako U a vrátí jej. Pokud ne, vrátí never.

Pokročilé případy použití infer

1. Odvození parametrů konstruktoru

Můžete použít infer k extrakci typů parametrů funkce konstruktoru:

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]

V tomto případě bere ConstructorParameters<T> typ funkce konstruktoru T. Odvodí typy parametrů konstruktoru jako P a vrátí je jako n-tici.

2. Extrakce vlastností z typů objektů

infer lze také použít k extrakci specifických vlastností z typů objektů pomocí mapovaných typů a podmíněných typů:

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; }

//An interface representing geographic coordinates.
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; }

Zde PickByType<T, K, U> vytvoří nový typ, který zahrnuje pouze vlastnosti T (s klíči v K), jejichž hodnoty jsou přiřaditelné typu U. Mapovaný typ iteruje přes klíče T a podmíněný typ filtruje klíče, které neodpovídají zadanému typu.

3. Práce s přísliby

Můžete odvodit vyřešený typ 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[]

Typ Awaited<T> bere typ T, u kterého se očekává, že bude Promise. Typ poté odvodí vyřešený typ U z Promise a vrátí jej. Pokud T není promise, vrátí T. Toto je vestavěný utilitní typ v novějších verzích TypeScriptu.

4. Extrakce typu pole příslibů

Kombinací Awaited a odvození typu pole můžete odvodit typ vyřešený polem Příslibů. To je zvláště užitečné při práci s 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]

Tento příklad nejprve definuje dvě asynchronní funkce, getUSDRate a getEURRate, které simulují načítání směnných kurzů. Utilitní typ PromiseArrayReturnType pak extrahuje vyřešený typ z každého Promise v poli, což vede k typu n-tice, kde je každý prvek očekávaným typem odpovídajícího Promise.

Praktické příklady napříč různými doménami

1. E-commerce aplikace

Zvažte e-commerce aplikaci, kde načítáte podrobnosti o produktech z API. Můžete použít infer k extrakci typu dat produktu:

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);

V tomto příkladu definujeme rozhraní Product a funkci fetchProduct, která načítá podrobnosti o produktu z API. Používáme Awaited a ReturnType k extrakci typu Product z návratového typu funkce fetchProduct, což nám umožňuje type-checkovat funkci displayProductDetails.

2. Internacionalizace (i18n)

Předpokládejme, že máte překladovou funkci, která vrací různé řetězce na základě národního prostředí. Můžete použít infer k extrakci návratového typu této funkce pro bezpečnost typu:

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!

Zde se TranslationType odvodí jako rozhraní Translations, což zajišťuje, že funkce greetUser má správné informace o typu pro přístup k přeloženým řetězcům.

3. Zpracování odpovědí z API

Při práci s API může být struktura odpovědi složitá. infer může pomoci extrahovat konkrétní datové typy z vnořených odpovědí 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);
  }
});

V tomto příkladu definujeme rozhraní ApiResponse a rozhraní UserData. Používáme infer a indexování typu k extrakci UserProfileType z odpovědi API, což zajišťuje, že funkce displayUserProfile obdrží správný typ.

Osvědčené postupy pro používání infer

Běžné nástrahy

Alternativy k infer

I když je infer výkonný nástroj, existují situace, kdy mohou být vhodnější alternativní přístupy:

Závěr

Klíčové slovo infer v TypeScriptu v kombinaci s podmíněnými typy odemyká pokročilé možnosti manipulace s typy. Umožňuje vám extrahovat konkrétní typy z komplexních typových struktur, což vám umožňuje psát robustnější, udržovatelnější a typově bezpečnější kód. Od odvozování návratových typů funkcí po extrahování vlastností z typů objektů jsou možnosti obrovské. Pochopením principů a osvědčených postupů uvedených v této příručce můžete využít infer naplno a zvýšit své dovednosti TypeScriptu. Nezapomeňte dokumentovat své typy, důkladně je otestovat a zvážit alternativní přístupy, pokud je to vhodné. Ovládnutí infer vám umožní psát skutečně expresivní a výkonný kód TypeScriptu, což v konečném důsledku povede k lepšímu softwaru.