Fortgeschrittene TypeScript Generics: Bedingungen, Abbildungen und Schlussfolgerungen beherrschen
Grace Collins
Solutions Engineer · Leapcell

Mehr als nur einfache Generics: Das Potenzial von TypeScript freisetzen
Typsystem von TypeScript ist unglaublich leistungsfähig, und seine generischen Fähigkeiten sind ein Eckpfeiler für die Erstellung flexibler und wiederverwendbarer Codes. Während einfache Generics es uns ermöglichen, Funktionen und Klassen zu schreiben, die mit verschiedenen Datentypen funktionieren, entfaltet sich die wahre Magie, wenn wir uns mit fortgeschritteneren Funktionen wie bedingten Typen, abgebildeten Typen und dem infer
-Schlüsselwort befassen. Diese Konstrukte ermöglichen es uns, Typen zu definieren, die sich dynamisch basierend auf ihren Eingaben anpassen, was zu hochgradig robusten, ausdrucksstarken und wartbaren Codebasen führt. In einer immer komplexer werdenden Webentwicklungslandschaft, in der Typsicherheit und Codequalität von größter Bedeutung sind, ist die Beherrschung dieser fortgeschrittenen generischen Techniken keine Luxus mehr, sondern eine Notwendigkeit für jeden ernsthaften TypeScript-Entwickler. Dieser Artikel wird diese leistungsstarken Funktionen entmystifizieren, ihre praktischen Anwendungen veranschaulichen und Sie anleiten, ihr volles Potenzial auszuschöpfen.
Entpacken fortgeschrittener generischer Typen
Bevor wir uns mit den Feinheiten von bedingten Typen, abgebildeten Typen und infer
befassen, wollen wir unser Verständnis dessen festigen, was Typen und Generics in TypeScript grundlegend darstellen. Im Wesentlichen beschreibt ein Typ die Form und das Verhalten von Werten. Generics hingegen sind wie Typvariablen, die es uns ermöglichen, Funktionen, Klassen und Schnittstellen zu schreiben, die mit jedem Typ arbeiten können und Flexibilität bieten, ohne die Typsicherheit zu opfern. Lassen Sie uns nun die fortgeschrittenen Konzepte untersuchen.
Bedingte Typen
Bedingte Typen ermöglichen es uns, basierend auf einer Bedingung, die eine Typbeziehung auswertet, zwischen zwei verschiedenen Typen zu wählen. Sie verwenden hauptsächlich das extends
-Schlüsselwort und haben eine Syntax, die einem JavaScript-Ternäroperator ähnelt: SomeType extends OtherType ? TrueType : FalseType
.
Prinzip: Die Kernidee besteht darin, eine Typprüfung durchzuführen. Wenn SomeType
zu OtherType
erweiterbar ist (was bedeutet, dass SomeType
eine Unterart oder identisch mit OtherType
ist), wird TrueType
ausgewählt; andernfalls wird FalseType
ausgewählt.
Implementierung und Anwendung: Betrachten Sie ein Szenario, in dem Sie den Rückgabetyp einer Funktion extrahieren möchten, jedoch nur, wenn die Eingabe tatsächlich eine Funktion ist.
type NonFunction = string | number | boolean; type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : NonFunction; // Beispielverwendung: function sum(a: number, b: number): number { return a + b; } type SumReturnType = GetReturnType<typeof sum>; // number const myString = "hello"; type StringReturnType = GetReturnType<typeof myString>; // NonFunction (weil typeof myString keine Funktion ist) function greet(name: string): void { console.log(`Hello, ${name}`); } type GreetReturnType = GetReturnType<typeof greet>; // void
In diesem Beispiel ist GetReturnType<T>
ein bedingter Typ. Er prüft, ob T
(...args: any[]) => infer R
erweitert. Wenn T
ein Funktionstyp ist, erfasst das infer R
-Schlüsselwort (das wir als nächstes besprechen werden) seinen Rückgabetyp, und R
wird dann ausgewählt. Andernfalls wird NonFunction
ausgewählt. Dies ist unglaublich nützlich für das Schreiben typsicherer Hilfsfunktionen, die auf anderen Typen operieren.
Ein weiterer gängiger Anwendungsfall ist die Typfilterung:
type FilterString<T> = T extends string ? T : never; type MixedTuple = [1, "hello", true, "world"]; type OnlyStringsFromTuple = FilterString<MixedTuple[number]>; // "hello" | "world"
Hier erstellt MixedTuple[number]
eine Union von 1 | "hello" | true | "world"
. FilterString
durchläuft dann jeden Typ in der Union, und nur string
-Typen bestehen die Bedingung, was zu "hello" | "world"
führt.
Abgebildete Typen
Abgebildete Typen ermöglichen es uns, Eigenschaften eines bestehenden Objekttyps in einen neuen Objekttyp zu transformieren. Sie durchlaufen im Wesentlichen die Schlüssel eines gegebenen Typs und wenden eine Transformation auf den Typ jeder Eigenschaft an.
Prinzip: Die Kernsyntax beinhaltet das Durchlaufen von Schlüsseln mit [P in keyof T]
, wobei K
eine Union von Eigenschaftsschlüsseln ist, die typischerweise aus keyof SomeType
gewonnen wird. Innerhalb der Abbildung können Sie den Eigenschaftsnamen (mithilfe von as
für die Schlüsselneuzuordnung) und/oder seinen Typ modifizieren.
Implementierung und Anwendung:
Ein gängiges Szenario ist das Markieren aller Eigenschaften eines Objekts als readonly
oder optional
. TypeScript bietet integrierte abgebildete Typen wie Partial<T>
und Readonly<T>
, aber das Verständnis ihres zugrundeliegenden Mechanismus ist entscheidend für die Erstellung eigener.
type Coordinates = { x: number; y: number; z: number; }; // Beispiel 1: Alle Eigenschaften auf null setzen type Nullable<T> = { [P in keyof T]: T[P] | null; }; type NullableCoordinates = Nullable<Coordinates>; /* type NullableCoordinates = { x: number | null; y: number | null; z: number | null; } */ // Beispiel 2: Alle Eigenschaften optional und schreibgeschützt machen type DeepReadonlyAndOptional<T> = { readonly [P in keyof T]?: T[P]; }; type ReadonlyOptionalCoordinates = DeepReadonlyAndOptional<Coordinates>; /* type ReadonlyOptionalCoordinates = { readonly x?: number; readonly y?: number; readonly z?: number; } */
Schlüsselneuzuordnung mit as
:
Abgebildete Typen können auch Eigenschaften umbenennen. Dies ist leistungsstark für die Transformation von Datenstrukturen.
type User = { id: string; name: string; email: string; }; // Schlüssel in Großbuchstaben umwandeln type UppercaseKeys<T> = { [P in keyof T as Uppercase<P & string>]: T[P]; }; type UserUppercaseKeys = UppercaseKeys<User>; /* type UserUppercaseKeys = { ID: string; NAME: string; EMAIL: string; } */ // Schlüssel umwandeln, um "id" zu entfernen und "Ref" hinzuzufügen type PropRef<T> = { [P in keyof T as P extends `${infer K}Id` ? `${K}Ref` : P]: T[P]; }; type Product = { productId: string; name: string }; type ProductRef = PropRef<Product>; /* type ProductRef = { productRef: string; name: string; } */
Die Schlüsselneuzuordnung eröffnet Möglichkeiten für komplexe Datenmigrationen und API-Vertrags transformationen auf Typenebene.
Das infer
-Schlüsselwort
Das infer
-Schlüsselwort wird immer innerhalb der extends
-Klausel eines bedingten Typs verwendet. Sein Zweck ist es, eine neue Typvariable zu deklarieren, die dann aus dem zu prüfenden Typ abgeleitet werden kann.
Prinzip: infer
fungiert als Platzhalter für einen Typ, den TypeScript während einer Typprüfung aus einer bestimmten Position innerhalb eines anderen Typs ableitet. Sobald abgeleitet, kann diese neue Typvariable im TrueType
-Zweig des bedingten Typs verwendet werden.
Implementierung und Anwendung:
Wir haben infer
bereits bei GetReturnType<T>
in Aktion gesehen. Lassen Sie uns weitere Beispiele untersuchen, insbesondere mit Array- und Promise-Typen.
Ableiten von Array-Elementtypen:
type GetArrayElementType<T> = T extends (infer U)[] ? U : never; type Numbers = number[]; type ElementOfNumbers = GetArrayElementType<Numbers>; // number type Strings = string[]; type ElementOfStrings = GetArrayElementType<Strings>; // string type NotAnArray = string; type ElementOfNotAnArray = GetArrayElementType<NotAnArray>; // never
Hier prüft T extends (infer U)[]
, ob T
ein Array-Typ ist. Wenn ja, erfasst infer U
den Typ der Elemente in diesem Array, und U
wird zum Ergebnis.
Ableiten von Promise-aufgelösten Werten:
type GetPromiseResolvedType<T> = T extends Promise<infer U> ? U : T; type MyPromise = Promise<string>; type PromiseResult = GetPromiseResolvedType<MyPromise>; // string type AnotherPromise = Promise<number[]>; type AnotherPromiseResult = GetPromiseResolvedType<AnotherPromise>; // number[] type NotAPromise = boolean; type NotAPromiseResult = GetPromiseResolvedType<NotAPromise>; // boolean
Dieser Utility-Typ ist unglaublich nützlich, wenn mit asynchronem Code gearbeitet wird, und ermöglicht es Ihnen, die .then()
-Handler korrekt zu typisieren.
Ableiten von Funktionsparametern:
type GetFunctionParameters<T> = T extends (...args: infer P) => any ? P : never; function doSomething(name: string, age: number): string { return `Name: ${name}, Age: ${age}`; } type DoSomethingParams = GetFunctionParameters<typeof doSomething>; // [name: string, age: number] type SomeOtherFunction = (a: boolean) => void; type SomeOtherFunctionParams = GetFunctionParameters<SomeOtherFunction>; // [a: boolean]
Dies ermöglicht es Ihnen, das vollständige Tuple von Parametern für einen gegebenen Funktionstyp zu extrahieren, was für höherstufige Funktionen oder Mocking nützlich sein kann.
Kombination von Konzepten: Ein realer Anwendungsfall
Lassen Sie uns diese Konzepte kombinieren, um einen anspruchsvolleren Typ zu erstellen. Stellen Sie sich vor, Sie haben eine Reihe von API-Endpunkten, die als Funktionen definiert sind. Sie möchten ihre Rückgabetypen extrahieren, wenn es sich um Promises handelt, andernfalls behalten Sie sie bei.
type APIResponseMapping = { getUser: (id: string) => Promise<{ id: string; name: string }>; getProducts: () => Promise<Array<{ id: string; name: string; price: number }>>; logEvent: (event: string) => void; // Kein Promise }; // Hilfsfunktion zum Abrufen des aufgelösten Typs eines Promises oder des Typs selbst type UnpackPromise<T> = T extends Promise<infer U> ? U : T; // Abgebildeter Typ zur Transformation von API-Endpunkt-Rückgabetypen type ResolvedAPIResponses<T> = { [K in keyof T]: T[K] extends (...args: any[]) => infer R ? UnpackPromise<R> : never; }; type ProcessedResponses = ResolvedAPIResponses<APIResponseMapping>; /* type ProcessedResponses = { getUser: { id: string; name: string; }; getProducts: { id: string; name: string; price: number; }[]; logEvent: void; } */
In diesem fortgeschrittenen Beispiel ist ResolvedAPIResponses
ein abgebildeter Typ, der über die Schlüssel von APIResponseMapping
iteriert. Für jede Eigenschaft K
prüft er zuerst mit einem bedingten Typ, ob T[K]
eine Funktion ist. Wenn ja, leitet er den Rückgabetyp R
ab. Dann wendet er den bedingten Typ UnpackPromise
auf R
an, um den endgültigen aufgelösten Typ zu erhalten. Wenn T[K]
keine Funktion ist, wird standardmäßig never
verwendet. Dies zeigt, wie diese fortgeschrittenen generischen Funktionen miteinander verknüpft werden können, um hochspezifische und nützliche Typtransformationen zu erstellen.
Die Macht der Präzision
Bedingte Typen, abgebildete Typen und das infer
-Schlüsselwort sind keine bloßen akademischen Kuriositäten; sie sind wesentliche Werkzeuge für das Schreiben wirklich typsicherer, flexibler und wartbarer TypeScript-Anwendungen. Sie ermöglichen es Ihnen, komplexe Typbeziehungen auszudrücken, Datenformen auf Typenebene zu transformieren und spezifische Typinformationen zu extrahieren, was zu Code führt, der sowohl robust als auch angenehm zu handhaben ist. Die Beherrschung dieser fortgeschrittenen generischen Techniken bewegt Sie über grundlegende Typsicherheit hinaus in den Bereich der leistungsstarken typgesteuerten Entwicklung, sodass Sie Fehler zur Kompilierzeit erkennen können, die sonst erst zur Laufzeit auftreten würden. Nehmen Sie diese Konzepte an und schöpfen Sie das volle Potenzial von TypeScript in Ihren Projekten aus.