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
- Zjednodušte: Používejte
infer
pouze v případě potřeby. Nadměrné používání může ztížit čtení a pochopení vašeho kódu. - Dokumentujte své typy: Přidejte komentáře, které vysvětlují, co dělají vaše podmíněné typy a příkazy
infer
. - Testujte své typy: Použijte kontrolu typu TypeScriptu, abyste se ujistili, že se vaše typy chovají podle očekávání.
- Zvažte výkon: Složité podmíněné typy mohou někdy ovlivnit dobu kompilace. Mějte na paměti složitost svých typů.
- Používejte utilitní typy: TypeScript poskytuje několik vestavěných utilitních typů (např.
ReturnType
,Awaited
), které mohou zjednodušit váš kód a snížit potřebu vlastních příkazůinfer
.
Běžné nástrahy
- Nesprávné odvození: Někdy může TypeScript odvodit typ, který neočekáváte. Dvakrát zkontrolujte definice a podmínky typu.
- Cyklické závislosti: Buďte opatrní při definování rekurzivních typů pomocí
infer
, protože mohou vést k cyklickým závislostem a chybám kompilace. - Příliš složité typy: Vyvarujte se vytváření příliš složitých podmíněných typů, kterým je obtížné porozumět a udržovat. Rozdělte je na menší, zvládnutelnější typy.
Alternativy k infer
I když je infer
výkonný nástroj, existují situace, kdy mohou být vhodnější alternativní přístupy:
- Tvrdit typy: V některých případech můžete použít tvrzení typu k explicitnímu určení typu hodnoty namísto jeho odvozování. Buďte však opatrní s tvrzeními typu, protože mohou obejít kontrolu typu.
- Typové stráže: Typové stráže lze použít ke zúžení typu hodnoty na základě kontrol za běhu. To je užitečné, když potřebujete zpracovat různé typy na základě podmínek za běhu.
- Utilitní typy: TypeScript poskytuje bohatou sadu utilitních typů, které mohou zpracovat mnoho běžných úloh manipulace s typy bez nutnosti vlastních příkazů
infer
.
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.