Ein umfassender Leitfaden zum TypeScript 'infer'-Schlüsselwort, der erklärt, wie man es mit bedingten Typen für leistungsstarke Typ-Extraktion und -Manipulation verwendet.
TypeScript Infer meistern: Bedingte Typ-Extraktion für fortgeschrittene Typ-Manipulation
Das Typsystem von TypeScript ist unglaublich leistungsstark und ermöglicht es Entwicklern, robuste und wartbare Anwendungen zu erstellen. Eine der Schlüsselfunktionen, die diese Leistungsfähigkeit ermöglicht, ist das infer
-Schlüsselwort in Verbindung mit bedingten Typen. Diese Kombination bietet einen Mechanismus zum Extrahieren spezifischer Typen aus komplexen Typstrukturen. Dieser Blogbeitrag befasst sich eingehend mit dem infer
-Schlüsselwort, erklärt seine Funktionalität und zeigt fortgeschrittene Anwendungsfälle. Wir werden praktische Beispiele untersuchen, die auf verschiedene Softwareentwicklungsszenarien anwendbar sind, von der API-Interaktion bis zur Manipulation komplexer Datenstrukturen.
Was sind bedingte Typen?
Bevor wir uns mit infer
befassen, wollen wir kurz die bedingten Typen wiederholen. Bedingte Typen in TypeScript ermöglichen es Ihnen, einen Typ basierend auf einer Bedingung zu definieren, ähnlich einem ternären Operator in JavaScript. Die grundlegende Syntax lautet:
T extends U ? X : Y
Das liest sich so: „Wenn der Typ T
dem Typ U
zugewiesen werden kann, dann ist der Typ X
; andernfalls ist der Typ Y
.“
Beispiel:
type IsString<T> = T extends string ? true : false;
type StringResult = IsString<string>; // type StringResult = true
type NumberResult = IsString<number>; // type NumberResult = false
Einführung des infer
-Schlüsselworts
Das infer
-Schlüsselwort wird innerhalb der extends
-Klausel eines bedingten Typs verwendet, um eine Typvariable zu deklarieren, die aus dem zu prüfenden Typ abgeleitet (inferiert) werden kann. Im Wesentlichen ermöglicht es Ihnen, einen Teil eines Typs für die spätere Verwendung zu „erfassen“.
Grundlegende Syntax:
type MyType<T> = T extends (infer U) ? U : never;
In diesem Beispiel versucht TypeScript, den Typ von U
abzuleiten, wenn T
einem beliebigen Typ zugewiesen werden kann. Wenn die Ableitung erfolgreich ist, ist der Typ U
; andernfalls ist er never
.
Einfache Beispiele für infer
1. Ableiten des Rückgabetyps einer Funktion
Ein häufiger Anwendungsfall ist die Ableitung des Rückgabetyps einer Funktion:
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
In diesem Beispiel nimmt ReturnType<T>
einen Funktionstyp T
als Eingabe. Es prüft, ob T
einer Funktion zugewiesen werden kann, die beliebige Argumente akzeptiert und einen Wert zurückgibt. Wenn ja, leitet es den Rückgabetyp als R
ab und gibt ihn zurück. Andernfalls gibt es any
zurück.
2. Ableiten des Elementtyps eines Arrays
Ein weiteres nützliches Szenario ist das Extrahieren des Elementtyps aus einem Array:
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
Hier prüft ArrayElementType<T>
, ob T
ein Array-Typ ist. Wenn ja, leitet es den Elementtyp als U
ab und gibt ihn zurück. Wenn nicht, gibt es never
zurück.
Fortgeschrittene Anwendungsfälle von infer
1. Ableiten der Parameter eines Konstruktors
Sie können infer
verwenden, um die Parametertypen einer Konstruktorfunktion zu extrahieren:
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]
In diesem Fall nimmt ConstructorParameters<T>
einen Konstruktorfunktionstyp T
. Es leitet die Typen der Konstruktorparameter als P
ab und gibt sie als Tupel zurück.
2. Extrahieren von Eigenschaften aus Objekttypen
infer
kann auch verwendet werden, um spezifische Eigenschaften aus Objekttypen mithilfe von gemappten Typen und bedingten Typen zu extrahieren:
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; }
//Ein Interface, das geografische Koordinaten darstellt.
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; }
Hier erstellt PickByType<T, K, U>
einen neuen Typ, der nur die Eigenschaften von T
(mit Schlüsseln in K
) enthält, deren Werte dem Typ U
zugewiesen werden können. Der gemappte Typ iteriert über die Schlüssel von T
, und der bedingte Typ filtert die Schlüssel heraus, die nicht dem angegebenen Typ entsprechen.
3. Arbeiten mit Promises
Sie können den aufgelösten Typ eines Promise
ableiten:
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[]
Der Awaited<T>
-Typ nimmt einen Typ T
entgegen, von dem erwartet wird, dass er ein Promise ist. Der Typ leitet dann den aufgelösten Typ U
des Promise ab und gibt ihn zurück. Wenn T
kein Promise ist, gibt er T zurück. Dies ist ein integrierter Utility-Typ in neueren Versionen von TypeScript.
4. Extrahieren des Typs eines Arrays von Promises
Die Kombination von Awaited
und der Array-Typ-Ableitung ermöglicht es Ihnen, den Typ abzuleiten, der von einem Array von Promises aufgelöst wird. Dies ist besonders nützlich im Umgang mit 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]
Dieses Beispiel definiert zunächst zwei asynchrone Funktionen, getUSDRate
und getEURRate
, die das Abrufen von Wechselkursen simulieren. Der Utility-Typ PromiseArrayReturnType
extrahiert dann den aufgelösten Typ aus jedem Promise
im Array, was zu einem Tupel-Typ führt, bei dem jedes Element der `awaited`-Typ des entsprechenden Promise ist.
Praktische Beispiele aus verschiedenen Bereichen
1. E-Commerce-Anwendung
Stellen Sie sich eine E-Commerce-Anwendung vor, in der Sie Produktdetails von einer API abrufen. Sie können infer
verwenden, um den Typ der Produktdaten zu extrahieren:
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> {
// API-Aufruf simulieren
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);
In diesem Beispiel definieren wir ein Product
-Interface und eine fetchProduct
-Funktion, die Produktdetails von einer API abruft. Wir verwenden Awaited
und ReturnType
, um den Product
-Typ aus dem Rückgabetyp der fetchProduct
-Funktion zu extrahieren, was uns ermöglicht, die displayProductDetails
-Funktion typsicher zu machen.
2. Internationalisierung (i18n)
Angenommen, Sie haben eine Übersetzungsfunktion, die je nach Gebietsschema unterschiedliche Zeichenfolgen zurückgibt. Sie können infer
verwenden, um den Rückgabetyp dieser Funktion zur Gewährleistung der Typsicherheit zu extrahieren:
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'); // Ausgabe: Bienvenue, Jean!
Hier wird TranslationType
als das Translations
-Interface abgeleitet, wodurch sichergestellt wird, dass die greetUser
-Funktion die korrekten Typinformationen für den Zugriff auf übersetzte Zeichenfolgen hat.
3. Umgang mit API-Antworten
Bei der Arbeit mit APIs kann die Antwortstruktur komplex sein. infer
kann helfen, spezifische Datentypen aus verschachtelten API-Antworten zu extrahieren:
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>> {
// API-Aufruf simulieren
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);
}
});
In diesem Beispiel definieren wir ein ApiResponse
-Interface und ein UserData
-Interface. Wir verwenden infer
und Typ-Indizierung, um den UserProfileType
aus der API-Antwort zu extrahieren und sicherzustellen, dass die displayUserProfile
-Funktion den korrekten Typ erhält.
Best Practices für die Verwendung von infer
- Halten Sie es einfach: Verwenden Sie
infer
nur, wenn es notwendig ist. Eine übermäßige Nutzung kann Ihren Code schwerer lesbar und verständlich machen. - Dokumentieren Sie Ihre Typen: Fügen Sie Kommentare hinzu, um zu erklären, was Ihre bedingten Typen und
infer
-Anweisungen tun. - Testen Sie Ihre Typen: Verwenden Sie die Typüberprüfung von TypeScript, um sicherzustellen, dass sich Ihre Typen wie erwartet verhalten.
- Berücksichtigen Sie die Leistung: Komplexe bedingte Typen können sich manchmal auf die Kompilierungszeit auswirken. Achten Sie auf die Komplexität Ihrer Typen.
- Verwenden Sie Utility-Typen: TypeScript bietet mehrere integrierte Utility-Typen (z. B.
ReturnType
,Awaited
), die Ihren Code vereinfachen und den Bedarf an benutzerdefinierteninfer
-Anweisungen reduzieren können.
Häufige Fallstricke
- Falsche Ableitung: Manchmal leitet TypeScript möglicherweise einen Typ ab, der nicht Ihren Erwartungen entspricht. Überprüfen Sie Ihre Typdefinitionen und Bedingungen.
- Zirkuläre Abhängigkeiten: Seien Sie vorsichtig beim Definieren rekursiver Typen mit
infer
, da dies zu zirkulären Abhängigkeiten und Kompilierungsfehlern führen kann. - Übermäßig komplexe Typen: Vermeiden Sie die Erstellung übermäßig komplexer bedingter Typen, die schwer zu verstehen und zu warten sind. Teilen Sie sie in kleinere, besser handhabbare Typen auf.
Alternativen zu infer
Obwohl infer
ein leistungsstarkes Werkzeug ist, gibt es Situationen, in denen alternative Ansätze besser geeignet sein könnten:
- Typzusicherungen (Type Assertions): In einigen Fällen können Sie Typzusicherungen verwenden, um den Typ eines Wertes explizit anzugeben, anstatt ihn abzuleiten. Seien Sie jedoch vorsichtig mit Typzusicherungen, da sie die Typüberprüfung umgehen können.
- Typ-Wächter (Type Guards): Typ-Wächter können verwendet werden, um den Typ eines Wertes basierend auf Laufzeitprüfungen einzugrenzen. Dies ist nützlich, wenn Sie verschiedene Typen basierend auf Laufzeitbedingungen behandeln müssen.
- Utility-Typen: TypeScript bietet eine umfangreiche Sammlung von Utility-Typen, die viele gängige Aufgaben der Typmanipulation ohne die Notwendigkeit benutzerdefinierter
infer
-Anweisungen bewältigen können.
Fazit
Das infer
-Schlüsselwort in TypeScript schaltet in Kombination mit bedingten Typen fortgeschrittene Fähigkeiten zur Typmanipulation frei. Es ermöglicht Ihnen, spezifische Typen aus komplexen Typstrukturen zu extrahieren, sodass Sie robusteren, wartbareren und typsicheren Code schreiben können. Von der Ableitung von Funktionsrückgabetypen bis zur Extraktion von Eigenschaften aus Objekttypen sind die Möglichkeiten riesig. Indem Sie die in diesem Leitfaden beschriebenen Prinzipien und Best Practices verstehen, können Sie infer
voll ausschöpfen und Ihre TypeScript-Fähigkeiten verbessern. Denken Sie daran, Ihre Typen zu dokumentieren, sie gründlich zu testen und bei Bedarf alternative Ansätze in Betracht zu ziehen. Das Meistern von infer
befähigt Sie, wirklich ausdrucksstarken und leistungsstarken TypeScript-Code zu schreiben, was letztendlich zu besserer Software führt.