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
- Zachowaj Prostotę: Używaj
infer
tylko wtedy, gdy jest to konieczne. Nadmierne używanie go może utrudnić czytanie i zrozumienie kodu. - Dokumentuj Swoje Typy: Dodaj komentarze, aby wyjaśnić, co robią twoje typy warunkowe i instrukcje
infer
. - Testuj Swoje Typy: Użyj sprawdzania typów TypeScript, aby upewnić się, że twoje typy zachowują się zgodnie z oczekiwaniami.
- Rozważ Wydajność: Złożone typy warunkowe mogą czasami wpływać na czas kompilacji. Pamiętaj o złożoności swoich typów.
- Używaj Typów Narzędziowych: TypeScript udostępnia kilka wbudowanych typów narzędziowych (np.
ReturnType
,Awaited
), które mogą uprościć kod i zmniejszyć potrzebę stosowania niestandardowych instrukcjiinfer
.
Częste Pułapki
- Niepoprawne Wnioskowanie: Czasami TypeScript może wywnioskować typ, który nie jest tym, czego oczekujesz. Sprawdź dokładnie swoje definicje typów i warunki.
- Cykliczne Zależności: Zachowaj ostrożność podczas definiowania typów rekurencyjnych za pomocą
infer
, ponieważ mogą one prowadzić do cyklicznych zależności i błędów kompilacji. - Zbyt Złożone Typy: Unikaj tworzenia zbyt złożonych typów warunkowych, które są trudne do zrozumienia i utrzymania. Rozbij je na mniejsze, łatwiejsze do zarządzania typy.
Alternatywy dla infer
Chociaż infer
jest potężnym narzędziem, istnieją sytuacje, w których bardziej odpowiednie mogą być alternatywne podejścia:
- Asercje Typów: W niektórych przypadkach możesz użyć asercji typów, aby jawnie określić typ wartości zamiast go wnioskować. Należy jednak zachować ostrożność przy używaniu asercji typów, ponieważ mogą one pominąć sprawdzanie typów.
- Strażnicy Typów: Strażnicy typów mogą być używani do zawężania typu wartości na podstawie kontroli w czasie wykonywania. Jest to przydatne, gdy musisz obsługiwać różne typy na podstawie warunków w czasie wykonywania.
- Typy Narzędziowe: TypeScript udostępnia bogaty zestaw typów narzędziowych, które mogą obsługiwać wiele typowych zadań manipulacji typami bez potrzeby stosowania niestandardowych instrukcji
infer
.
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.