Dynamische Typinferenz aus API-Antworten mit dem infer-Schlüsselwort von TypeScript
Min-jun Kim
Dev Intern · Leapcell

Einführung
In der Welt der modernen Webentwicklung ist die Interaktion mit APIs eine tägliche Realität. Wir senden Anfragen und erhalten Antworten. Während die Daten selbst entscheidend sind, ist es ebenso wichtig, sicherzustellen, dass unsere lokale Anwendung die genaue Struktur dieser Daten versteht, um robusten, typsicheren Code zu gewährleisten. Das manuelle Definieren von Schnittstellen für jede einzelne API-Antwort kann schnell zu einer mühsamen und fehleranfälligen Aufgabe werden, insbesondere bei der Arbeit mit komplexen oder sich entwickelnden APIs. Dies führt oft dazu, dass Entwickler entweder brüchige, manuell geschriebene Definitionen pflegen oder, schlimmer noch, auf any zurückgreifen und dabei die Typsicherheit vollständig opfern.
TypeScript bietet jedoch eine leistungsstarke Funktion, das infer-Schlüsselwort, das die Art und Weise, wie wir API-Antworttypen behandeln, revolutionieren kann. Durch die Nutzung von infer können wir den Rückgabetyp direkt aus der Signatur einer API-Funktion dynamisch extrahieren und ableiten, was uns ermöglicht, widerstandsfähigere, weniger mit Boilerplate-Code belastete Anwendungen zu erstellen. Dieser Artikel befasst sich mit der praktischen Anwendung von infer, um das häufige Problem der dynamischen Ableitung von Rückgabetypen aus API-Antworten elegant zu lösen und die Entwicklererfahrung sowie die Wartbarkeit des Codes erheblich zu verbessern.
Kernkonzepte und Prinzipien
Bevor wir uns mit der praktischen Implementierung befassen, definieren wir kurz einige Schlüsselkonzepte von TypeScript, die unserer Lösung zugrunde liegen:
- Generics: Generics ermöglichen es uns, flexible, wiederverwendbare Codebausteine zu schreiben, die mit verschiedenen Typen arbeiten und gleichzeitig die Typsicherheit gewährleisten. Sie werden oft mit spitzen Klammern bezeichnet, wie z. B.
Array<T>oderPromise<T>. - Bedingte Typen: Diese Typen ermöglichen es uns, einen Typ basierend auf einer Bedingung auszuwählen. Ihre Syntax ähnelt einem ternären Operator:
Bedingung ? TypWennWahr : TypWennFalsch. infer-Schlüsselwort: Dies ist der Star unserer Show. Innerhalb eines bedingten Typs ermöglichtinferuns, einen Typ zu "erfassen", der Teil eines anderen Typs ist, und diesen erfassten Typ dann im "wahren" Zweig des bedingten Typs zu verwenden. Es ist ein leistungsstarker Mechanismus für das Pattern Matching auf Typen. Zum Beispiel würdeT extends Promise<infer U> ? U : Tden aufgelösten TypUaus einemPromise<U>extrahieren.ReturnType<T>-Dienstprogrammtyp: Dieser integrierte TypeScript-Dienstprogrammtyp extrahiert den Rückgabetyp eines FunktionstypsT. Zum Beispiel würdeReturnType<() => string>zustringaufgelöst werden. Während nützlich, hilft er uns nicht direkt bei Promises, die die meisten API-Aufrufe zurückgeben.Awaited<T>-Dienstprogrammtyp: Einführung in TypeScript 4.5,Awaited<T>extrahiert den "awaitable" Typ einesPromise<T>oder entpackt verschachtelte Promises rekursiv. Dies ist besonders relevant für API-Aufrufe, die Promises zurückgeben.
Dynamische Typinferenz in Aktion
Unser Ziel ist es, einen Dienstprogrammtyp zu erstellen, der angesichts einer asynchronen API-Funktion (die typischerweise ein Promise zurückgibt) dieses Promise effektiv "entpacken" und uns den Typ der Daten, zu denen es aufgelöst wird, liefern kann.
Stellen wir uns eine einfache API-Funktion vor:
// api.ts interface User { id: number; name: string; email: string; } interface Product { productId: string; productName: string; price: number; } async function fetchUser(userId: number): Promise<User> { // Simulate API call return { id: userId, name: 'John Doe', email: 'john@example.com' }; } async function fetchProducts(): Promise<Product[]> { // Simulate API call return [{ productId: 'P1', productName: 'Laptop', price: 1200 }]; }
Nun erstellen wir unseren InferApiResponse-Dienstprogrammtyp mit infer:
// utils.ts type InferApiResponse<T extends (...args: any[]) => Promise<any>> = T extends (...args: any[]) => Promise<infer R> ? R : never;
Lassen Sie uns diesen InferApiResponse-Typ aufschlüsseln:
T extends (...args: any[]) => Promise<any>: Dies ist eine Einschränkung. Sie stellt sicher, dass der TypT, der anInferApiResponseübergeben wird, eine Funktion sein muss, die einPromisevon beliebigen Typen zurückgibt. Das ist entscheidend, da wir uns speziell auf asynchrone API-Funktionen konzentrieren.T extends (...args: any[]) => Promise<infer R>: Dies ist der bedingte Typ, bei dem die Magie passiert.- Wir prüfen, ob
T(unser API-Funktionstyp) einem Funktionstyp zugewiesen werden kann, der einPromisezurückgibt. - Entscheidend ist, dass
infer RTypeScript sagt: „Wenn der Rückgabetyp dieser Funktion einPromiseist, dann inferiere bitte den Typ, zu dem das Promise aufgelöst wird, und weise ihn der neuen TypvariablenRzu."
- Wir prüfen, ob
? R : never:- Wenn die Bedingung wahr ist (d. h.
Tist eine Funktion, die einPromisezurückgibt, und wir habenRerfolgreich abgeleitet), dann löst sich unserInferApiResponse-Typ alsRauf (der aufgelöste Typ des Promises). - Wenn die Bedingung falsch ist (was nicht passieren sollte, wenn unsere anfängliche Einschränkung erfüllt ist), greifen wir auf
neverzurück, was einen unmöglichen Typ anzeigt.
- Wenn die Bedingung wahr ist (d. h.
Lassen Sie uns sehen, wie es funktioniert:
// app.ts import { fetchUser, fetchProducts } from './api'; import { InferApiResponse } from './utils'; // Geht davon aus, dass InferApiResponse in utils.ts definiert ist type UserApiResponse = InferApiResponse<typeof fetchUser>; // UserApiResponse wird korrekt als User abgeleitet type ProductsApiResponse = InferApiResponse<typeof fetchProducts>; // ProductsApiResponse wird korrekt als Product[] abgeleitet // Beispielverwendung: async function displayUser(userId: number) { const user: UserApiResponse = await fetchUser(userId); console.log(user.name); // Typsichere Anwendung // user.id, user.email sind ebenfalls mit korrekten Typen verfügbar } async function displayProducts() { const products: ProductsApiResponse = await fetchProducts(); console.log(products[0].productName); // Typsichere Anwendung // products[0].productId, products[0].price sind ebenfalls verfügbar } displayUser(1); displayProducts();
Wie Sie sehen können, wird UserApiResponse automatisch als User und ProductsApiResponse als Product[] abgeleitet, wodurch die Notwendigkeit, diese Schnittstellen für die Verwendung in anderen Teilen unserer Anwendung manuell neu zu tippen, vollständig entfällt.
Warum nicht einfach Awaited<ReturnType<typeof fetchUser>>?
Man könnte sich fragen, warum wir nicht einfach Awaited<ReturnType<typeof fetchUser>> verwenden. Das funktioniert auch und ist für einfache Fälle wohl prägnanter:
type UserApiResponse2 = Awaited<ReturnType<typeof fetchUser>>; // Löst sich ebenfalls zu User auf
Während Awaited<ReturnType<T>> für dieses spezielle Szenario perfekt funktioniert, ist InferApiResponse mit infer grundlegender und demonstriert die Leistung von infer für komplexere Typmanipulationen, bei denen Awaited oder ReturnType allein möglicherweise nicht ausreichen oder wo Sie Typen aus komplexeren Strukturen als nur dem Rückgabetyp einer Funktion extrahieren müssen. Unser benutzerdefinierter InferApiResponse erzwingt auch die Einschränkung, nur Funktionen zu akzeptieren, die Promises zurückgeben, was eine nützliche Absicherung sein kann.
Das infer-Schlüsselwort zeichnet sich in Situationen aus, in denen Sie Typen matern und einen bestimmten Teil dieses Musters extrahieren müssen. Awaited und ReturnType sind spezialisierte Dienstprogrammtypen, die auf ähnlichen Konzepten (wahrscheinlich intern mit infer) für gängige Muster aufgebaut sind. Das Verständnis von infer gibt Ihnen die Flexibilität, eigene spezialisierte Dienstprogrammtypen für einzigartige Szenarien zu erstellen.
Fazit
Das infer-Schlüsselwort in TypeScript ist ein bemerkenswert leistungsstarkes Werkzeug für die dynamische Typinferenz, insbesondere bei der Arbeit mit API-Antworten. Durch die Erstellung eines einfachen Dienstprogrammtyps können wir die genaue Struktur der von asynchronen API-Funktionen zurückgegebenen Daten automatisch ableiten, was den Boilerplate-Aufwand drastisch reduziert und die Typsicherheit in unseren Anwendungen erheblich verbessert. Dieser Ansatz optimiert nicht nur die Entwicklung, sondern gibt uns auch mehr Vertrauen in unseren Code, wenn sich APIs weiterentwickeln, und macht unsere JavaScript-Projekte robuster und wartbarer. Die Nutzung von infer hilft uns, intelligentere, sicherere und ausdrucksstärkere TypeScript-Codes zu schreiben, indem wir den Compiler die Hauptarbeit der Typableitung erledigen lassen.

