Hrvatski

Sveobuhvatan vodič za TypeScript ključnu riječ 'infer', koji objašnjava kako je koristiti s uvjetnim tipovima za moćno izdvajanje i manipulaciju tipovima, uključujući napredne primjere upotrebe.

Ovladavanje TypeScript Infer: Izdvajanje uvjetnih tipova za naprednu manipulaciju tipovima

TypeScriptov sustav tipova je nevjerojatno moćan, omogućujući developerima stvaranje robusnih i održivih aplikacija. Jedna od ključnih značajki koja omogućuje tu moć je ključna riječ infer koja se koristi u kombinaciji s uvjetnim tipovima. Ova kombinacija pruža mehanizam za izdvajanje specifičnih tipova iz složenih struktura tipova. Ovaj blog post duboko zaranja u ključnu riječ infer, objašnjavajući njezinu funkcionalnost i prikazujući napredne primjere upotrebe. Istražit ćemo praktične primjere primjenjive u različitim scenarijima razvoja softvera, od interakcije s API-jem do manipulacije složenim strukturama podataka.

Što su uvjetni tipovi?

Prije nego što zaronimo u infer, brzo ponovimo uvjetne tipove. Uvjetni tipovi u TypeScriptu omogućuju vam definiranje tipa na temelju uvjeta, slično ternarnom operatoru u JavaScriptu. Osnovna sintaksa je:

T extends U ? X : Y

Ovo se čita kao: "Ako je tip T dodjeljiv tipu U, onda je tip X; inače, tip je Y."

Primjer:

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

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

Uvođenje ključne riječi infer

Ključna riječ infer koristi se unutar extends klauzule uvjetnog tipa za deklariranje varijable tipa koja se može inferirati (zaključiti) iz tipa koji se provjerava. U suštini, omogućuje vam da "uhvatite" dio tipa za kasniju upotrebu.

Osnovna sintaksa:

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

U ovom primjeru, ako je T dodjeljiv nekom tipu, TypeScript će pokušati inferirati tip od U. Ako je inferencija uspješna, tip će biti U; inače, bit će never.

Jednostavni primjeri upotrebe infer

1. Inferiranje povratnog tipa funkcije

Čest slučaj upotrebe je inferiranje povratnog tipa funkcije:

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

U ovom primjeru, ReturnType<T> uzima tip funkcije T kao ulaz. Provjerava je li T dodjeljiv funkciji koja prihvaća bilo koje argumente i vraća vrijednost. Ako jest, inferira povratni tip kao R i vraća ga. Inače, vraća any.

2. Inferiranje tipa elementa polja

Još jedan koristan scenarij je izdvajanje tipa elementa iz polja:

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

Ovdje, ArrayElementType<T> provjerava je li T tip polja. Ako jest, inferira tip elementa kao U i vraća ga. Ako nije, vraća never.

Napredni primjeri upotrebe infer

1. Inferiranje parametara konstruktora

Možete koristiti infer za izdvajanje tipova parametara konstruktorske funkcije:

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]

U ovom slučaju, ConstructorParameters<T> uzima tip konstruktorske funkcije T. Inferira tipove parametara konstruktora kao P i vraća ih kao tuple.

2. Izdvajanje svojstava iz objektnih tipova

infer se također može koristiti za izdvajanje specifičnih svojstava iz objektnih tipova pomoću mapiranih i uvjetnih tipova:

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

//Sučelje koje predstavlja geografske koordinate.
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; }

Ovdje, PickByType<T, K, U> stvara novi tip koji uključuje samo ona svojstva od T (s ključevima u K) čije su vrijednosti dodjeljive tipu U. Mapirani tip iterira preko ključeva od T, a uvjetni tip filtrira ključeve koji ne odgovaraju navedenom tipu.

3. Rad s Promise objektima

Možete inferirati riješeni (resolved) tip Promise objekta:

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[]

Tip Awaited<T> uzima tip T, za koji se očekuje da je Promise. Tip zatim inferira riješeni tip U Promise objekta i vraća ga. Ako T nije Promise, vraća T. Ovo je ugrađeni pomoćni tip u novijim verzijama TypeScripta.

4. Izdvajanje tipa iz polja Promise objekata

Kombiniranje Awaited i inferiranja tipa polja omogućuje vam da inferirate tip koji rješava polje Promise objekata. Ovo je posebno korisno kod rada 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]

Ovaj primjer prvo definira dvije asinkrone funkcije, getUSDRate i getEURRate, koje simuliraju dohvaćanje tečajeva. Pomoćni tip PromiseArrayReturnType zatim izdvaja riješeni tip iz svakog Promise objekta u polju, što rezultira tuple tipom gdje je svaki element awaited tip odgovarajućeg Promise objekta.

Praktični primjeri u različitim domenama

1. Aplikacija za e-trgovinu

Uzmimo u obzir aplikaciju za e-trgovinu gdje dohvaćate detalje proizvoda s API-ja. Možete koristiti infer za izdvajanje tipa podataka o proizvodu:

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> {
  // Simulacija API poziva
  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);

U ovom primjeru definiramo sučelje Product i funkciju fetchProduct koja dohvaća detalje proizvoda s API-ja. Koristimo Awaited i ReturnType za izdvajanje tipa Product iz povratnog tipa funkcije fetchProduct, što nam omogućuje provjeru tipova u funkciji displayProductDetails.

2. Internazionalizacija (i18n)

Pretpostavimo da imate funkciju za prevođenje koja vraća različite stringove ovisno o lokalizaciji. Možete koristiti infer za izdvajanje povratnog tipa ove funkcije radi sigurnosti tipova:

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'); // Izlaz: Bienvenue, Jean!

Ovdje se TranslationType inferira kao sučelje Translations, osiguravajući da funkcija greetUser ima točne informacije o tipu za pristup prevedenim stringovima.

3. Rukovanje API odgovorima

Prilikom rada s API-jima, struktura odgovora može biti složena. infer može pomoći u izdvajanju specifičnih tipova podataka iz ugniježđenih API odgovora:

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>> {
  // Simulacija API poziva
  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);
  }
});

U ovom primjeru definiramo sučelje ApiResponse i sučelje UserData. Koristimo infer i indeksiranje tipova za izdvajanje UserProfileType iz API odgovora, osiguravajući da funkcija displayUserProfile prima ispravan tip.

Najbolje prakse za korištenje infer

Česte zamke

Alternative za infer

Iako je infer moćan alat, postoje situacije u kojima bi alternativni pristupi mogli biti prikladniji:

Zaključak

Ključna riječ infer u TypeScriptu, kada se kombinira s uvjetnim tipovima, otključava napredne mogućnosti manipulacije tipovima. Omogućuje vam izdvajanje specifičnih tipova iz složenih struktura tipova, što vam omogućuje pisanje robusnijeg, održivijeg i tipski sigurnijeg koda. Od inferiranja povratnih tipova funkcija do izdvajanja svojstava iz objektnih tipova, mogućnosti su ogromne. Razumijevanjem principa i najboljih praksi navedenih u ovom vodiču, možete iskoristiti infer do punog potencijala i podići svoje TypeScript vještine. Ne zaboravite dokumentirati svoje tipove, temeljito ih testirati i razmotriti alternativne pristupe kada je to prikladno. Ovladavanje ključnom riječi infer osnažuje vas za pisanje doista izražajnog i moćnog TypeScript koda, što u konačnici dovodi do boljeg softvera.