Polski

Kompletny przewodnik po słowie kluczowym 'infer' w TypeScript, wyjaśniający, jak używać go z typami warunkowymi do wydajnego wyodrębniania i manipulacji typami, w tym zaawansowane przypadki użycia.

Opanowanie TypeScript Infer: Warunkowe Wyodrębnianie Typów dla Zaawansowanej Manipulacji Typami

System typów TypeScript jest niezwykle potężny, umożliwiając programistom tworzenie solidnych i łatwych w utrzymaniu aplikacji. Jedną z kluczowych cech umożliwiających tę moc jest słowo kluczowe infer używane w połączeniu z typami warunkowymi. To połączenie zapewnia mechanizm wyodrębniania określonych typów ze złożonych struktur typów. Ten wpis na blogu zagłębia się w słowo kluczowe infer, wyjaśniając jego funkcjonalność i prezentując zaawansowane przypadki użycia. Zbadamy praktyczne przykłady mające zastosowanie w różnych scenariuszach tworzenia oprogramowania, od interakcji z API po złożoną manipulację strukturami danych.

Czym są Typy Warunkowe?

Zanim przejdziemy do infer, szybko przejrzyjmy typy warunkowe. Typy warunkowe w TypeScript umożliwiają zdefiniowanie typu na podstawie warunku, podobnie jak operator trójargumentowy w JavaScript. Podstawowa składnia to:

T extends U ? X : Y

To czyta się jako: "Jeśli typ T jest przypisywalny do typu U, to typem jest X; w przeciwnym razie typem jest Y."

Przykład:

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

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

Wprowadzenie do Słowa Kluczowego infer

Słowo kluczowe infer jest używane w klauzuli extends typu warunkowego do deklarowania zmiennej typu, która może być wywnioskowana z sprawdzanego typu. Zasadniczo pozwala to "uchwycić" część typu do późniejszego wykorzystania.

Podstawowa Składnia:

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

W tym przykładzie, jeśli T jest przypisywalne do jakiegoś typu, TypeScript spróbuje wywnioskować typ U. Jeśli wnioskowanie się powiedzie, typem będzie U; w przeciwnym razie będzie to never.

Proste Przykłady Użycia infer

1. Wnioskowanie Typu Zwracanego Funkcji

Częstym przypadkiem użycia jest wnioskowanie typu zwracanego funkcji:

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

W tym przykładzie, ReturnType<T> przyjmuje typ funkcji T jako dane wejściowe. Sprawdza, czy T jest przypisywalne do funkcji, która akceptuje dowolne argumenty i zwraca wartość. Jeśli tak, wnioskuje typ zwracany jako R i go zwraca. W przeciwnym razie zwraca any.

2. Wnioskowanie Typu Elementu Tablicy

Innym przydatnym scenariuszem jest wyodrębnianie typu elementu z tablicy:

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

Tutaj, ArrayElementType<T> sprawdza, czy T jest typem tablicowym. Jeśli tak, wnioskuje typ elementu jako U i go zwraca. Jeśli nie, zwraca never.

Zaawansowane Przypadki Użycia infer

1. Wnioskowanie Parametrów Konstruktora

Możesz użyć infer do wyodrębnienia typów parametrów funkcji konstruktora:

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]

W tym przypadku, ConstructorParameters<T> przyjmuje typ funkcji konstruktora T. Wnioskuje typy parametrów konstruktora jako P i zwraca je jako krotkę.

2. Wyodrębnianie Właściwości z Typów Obiektów

infer może być również używane do wyodrębniania określonych właściwości z typów obiektów przy użyciu typów mapowanych i typów warunkowych:

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

Tutaj, PickByType<T, K, U> tworzy nowy typ, który zawiera tylko właściwości T (z kluczami w K), których wartości są przypisywalne do typu U. Typ mapowany iteruje po kluczach T, a typ warunkowy odfiltrowuje klucze, które nie pasują do określonego typu.

3. Praca z Obietnicami

Możesz wywnioskować rozwiązany 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> przyjmuje typ T, który ma być Obietnicą. Następnie typ wnioskuje rozwiązany typ U Obietnicy i go zwraca. Jeśli T nie jest obietnicą, zwraca T. Jest to wbudowany typ narzędziowy w nowszych wersjach TypeScript.

4. Wyodrębnianie Typu Tablicy Obietnic

Połączenie wnioskowania typu tablicy i Awaited pozwala wywnioskować typ rozwiązywany przez tablicę Obietnic. Jest to szczególnie przydatne podczas pracy z 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]

Ten przykład najpierw definiuje dwie funkcje asynchroniczne, getUSDRate i getEURRate, które symulują pobieranie kursów wymiany. Typ narzędziowy PromiseArrayReturnType następnie wyodrębnia rozwiązany typ z każdej Promise w tablicy, co daje typ krotki, gdzie każdy element jest oczekiwanym typem odpowiedniej Obietnicy.

Praktyczne Przykłady w Różnych Domenach

1. Aplikacja E-commerce

Rozważmy aplikację e-commerce, w której pobierasz szczegóły produktu z API. Możesz użyć infer do wyodrębnienia typu danych 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);

W tym przykładzie definiujemy interfejs Product i funkcję fetchProduct, która pobiera szczegóły produktu z API. Używamy Awaited i ReturnType do wyodrębnienia typu Product z typu zwracanego funkcji fetchProduct, co pozwala nam typować funkcję displayProductDetails.

2. Internacjonalizacja (i18n)

Załóżmy, że masz funkcję tłumaczenia, która zwraca różne ciągi znaków w zależności od ustawień regionalnych. Możesz użyć infer do wyodrębnienia typu zwracanego tej funkcji w celu zapewnienia bezpieczeństwa typów:

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!

Tutaj, TranslationType jest wywnioskowany jako interfejs Translations, zapewniając, że funkcja greetUser ma poprawne informacje o typie do uzyskiwania dostępu do przetłumaczonych ciągów znaków.

3. Obsługa Odpowiedzi API

Podczas pracy z API struktura odpowiedzi może być złożona. infer może pomóc w wyodrębnieniu określonych typów danych z zagnieżdżonych odpowiedzi 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);
  }
});

W tym przykładzie definiujemy interfejs ApiResponse i interfejs UserData. Używamy infer i indeksowania typów do wyodrębnienia UserProfileType z odpowiedzi API, zapewniając, że funkcja displayUserProfile otrzyma poprawny typ.

Najlepsze Praktyki Używania infer

Częste Pułapki

Alternatywy dla infer

Chociaż infer jest potężnym narzędziem, istnieją sytuacje, w których bardziej odpowiednie mogą być alternatywne podejścia:

Wniosek

Słowo kluczowe infer w TypeScript, w połączeniu z typami warunkowymi, odblokowuje zaawansowane możliwości manipulacji typami. Pozwala wyodrębniać określone typy ze złożonych struktur typów, umożliwiając pisanie bardziej solidnego, łatwego w utrzymaniu i bezpiecznego pod względem typów kodu. Od wnioskowania typów zwracanych funkcji po wyodrębnianie właściwości z typów obiektów, możliwości są ogromne. Rozumiejąc zasady i najlepsze praktyki przedstawione w tym przewodniku, możesz wykorzystać infer w pełni i podnieść swoje umiejętności TypeScript. Pamiętaj, aby dokumentować swoje typy, dokładnie je testować i rozważać alternatywne podejścia, gdy jest to właściwe. Opanowanie infer umożliwia pisanie naprawdę ekspresyjnego i potężnego kodu TypeScript, co ostatecznie prowadzi do lepszego oprogramowania.

Opanowanie TypeScript Infer: Warunkowe Wyodrębnianie Typów dla Zaawansowanej Manipulacji Typami | MLOG