TypeScript-Power-Tools: extends, infer, keyof Maps und as Remaps
Erstellen Sie bibliotheksklasse Typen, indem Sie Bedingungen, Inferenz, mappierte Schleifen und Schlüsselummapping kombinieren – das gleiche Werkzeugset wie hinter Zod und tRPC.
Nach Monaten mit TypeScript reichen eingebaute Funktionen wie Partial, Pick und Omit letztendlich nicht mehr aus, um tatsächliche Anforderungen von Bibliotheken zu erfüllen – beispielsweise den Rückgabetyp einer Funktion extrahieren, jede Schlüsselwerte umwandeln oder Typen erstellen, die sich an andere Typen anpassen. Vier Funktionen zusammen ermöglichen es, dieses Niveau zu erreichen: bedingte extends-Anweisungen, infer, verschachtelte Schleifen über keyof sowie das Ummappen von Schlüsseln mit as. Für sich genommen sind sie leicht verständlich; gemeinsam bilden sie die Grundlage für Muster, wie man sie in Zod, tRPC und React Router findet.
1. Bedingte Typen mit extends
In typologischen Kontexten fungiert extends wie eine if-Anweisung:
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
Erklärung: Wenn T auf string übertragbar ist, wird true zurückgegeben, ansonsten false.
Konditionale werden stärker, wenn sie distributiv sind. Eine Vereinigung T wird Mitglied für Mitglied überprüft:
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// Result = string[] | number[] (not (string | number)[])
Deshalb ist das Filtern von Vereinigungen kurz:
type ExcludeString<T> = T extends string ? never : T;
type NoStrings = ExcludeString<string | number | boolean>;
// NoStrings = number | boolean
never verschwindet nicht aus Vereinigungen – das ist dieselbe Idee hinter der eingebauten Funktion Exclude<T, U>.
2. Formen mit infer extrahieren
infer kommt nur innerhalb einer extends-Klausel vor. Es deklariert einen Platzhalter, den TypeScript mit der passenden Form füllt. Klassische Extraktion des Rückgabetyps:
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { id: 1, name: "Oussama" };
}type User = MyReturnType<typeof getUser>;
// User = { id: number; name: string }
Schritte: typeof getUser mit (...args: any[]) => infer R abgleichen, R mit dem tatsächlichen Rückgabewert verknüpfen und schließlich auf R zurückgreifen.
infer funktioniert auch an anderen Stellen:
// Extract the element type of an array
type ElementOf<T> = T extends (infer U)[] ? U : never;
type Item = ElementOf<string[]>; // string
// Extract the resolved type of a Promise
type Awaited2<T> = T extends Promise<infer U> ? U : T;
type Data = Awaited2<Promise<{ status: number }>>;
// Data = { status: number }// Extract the first argument of a function
type FirstArg<T> = T extends (arg: infer A, ...rest: any[]) => any ? A : never;
type Arg = FirstArg<(id: number, name: string) => void>; // number
Rekursion entpackt verschachtelte Promises:
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
type Flat = DeepAwaited<Promise<Promise<Promise<string>>>>;
// Flat = string
3. Schleifen mit keyof bei mappierten Typen
keyof liefert eine Vereinigung der Schlüssel:
interface User {
id: number;
name: string;
email: string;
}
type UserKeys = keyof User; // "id" | "name" | "email"
Mappierte Typen iterieren über diese Vereinigung wie ein typbezogener for...in:
type Readonly2<T> = {
[K in keyof T]: T[K];
};
Fügen Sie Modifikatoren für optionale oder nur zum Lesen zugängliche Kopien hinzu:
// Make every property optional
type Optional<T> = {
[K in keyof T]?: T[K];
};
// Make every property readonly
type ReadonlyAll<T> = {
readonly [K in keyof T]: T[K];
};// Remove readonly / optional with a minus modifier
type Mutable<T> = {
-readonly [K in keyof T]-?: T[K];
};
Transformieren Sie Werte während der Schleife:
type Stringify<T> = {
[K in keyof T]: string;
};
type StringifiedUser = Stringify<User>;
// { id: string; name: string; email: string }
Kombinieren Sie dies mit Bedingungen:
type NullableStrings<T> = {
[K in keyof T]: T[K] extends string ? T[K] | null : T[K];
};
type Result2 = NullableStrings<User>;
// { id: number; name: string | null; email: string | null }
4. Neuanordnung von Schlüsseln mit as
In einem mappierten Typ berechnet as für jede Eigenschaft einen neuen Schlüsselnamen:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
/*
{
getId: () => number;
getName: () => string;
getEmail: () => string;
}
*/
[K in keyof T] iteriert; die Template-Literal erstellt Schlüssel im Stil von getName; T[K] wird zum Rückgabetyp des Getters.
Verwenden Sie as never, um während der Schleife Schlüssel wegzulassen:
type OmitByType<T, U> = {
[K in keyof T as T[K] extends U ? never : K]: T[K];
};
interface Mixed {
id: number;
name: string;
active: boolean;
}type OnlyNonBoolean = OmitByType<Mixed, boolean>;
// { id: number; name: string }
Dieses extends innerhalb des as-Musters ist die Methode, mit der wertbasierte Pick/Omit-Varianten erstellt werden.
Alle vier zusammen
Eine realistische Kombination: Validatoren für Datefelder, Überspringen von Funktionseigenschaften:
type Validators<T> = {
[K in keyof T as T[K] extends Function ? never : `validate${Capitalize<string & K>}`]:
(value: T[K]) => value is T[K];
};
interface ApiResponse {
id: number;
email: string;
active: boolean;
refresh: () => void;
}type ResponseValidators = Validators<ApiResponse>;
/*
{
validateId: (value: number) => value is number;
validateEmail: (value: string) => value is string;
validateActive: (value: boolean) => value is boolean;
}
*/
Aufschlüsselung: keyof liefert die Schlüssel; der abgebildete Schleifenlauf besucht jeden davon; as ändert die Namen oder lässt Elemente über never weg, wenn T[K] extends Function gilt; extends entscheidet, ob ein Element beibehalten oder weggelassen wird; infer würde verwendet werden, wenn nestede Argumenttypen extrahiert werden sollen.
Schnellreferenz
| Funktion | Rolle | Wo |
|---|---|---|
extends | Typebene-Prüfung | Vergleich von Typen |
infer | Aufnahme des abgeglichenen Elements | Nur innerhalb von extends |
[K in keyof T]asas-KlauselSobald man darauf klickt, zwingen komplexe Strukturen nicht mehr zu any. Baue Pick, Omit, Record und ReturnType von Grund auf neu auf – alle oben genannten Funktionen werden sichtbar – und das Werkzeugset auf Typenebene wird praktisch anstatt mystisch. Bibliotheksentwickler verlassen sich auf genau diese vier Mechanismen; das Lesen ihrer .d.ts-Dateien wird nach einem Wochenende des manuellen Neubaus der Hilfsfunktionen viel einfacher.
Übungsweg
Erstellen Sie Exclude mit distributiven Bedingungen neu, ReturnType mithilfe von infer, Partial/Readonly mit abgeleiteten Modifikatoren sowie eine benutzerdefinierte OmitByType mit as never. Diese vier Übungen bringen alle Funktionen dieses Leitfadens zum Einsatz. Danach wirkt das Lesen von .d.ts-Dateien einer Bibliothek nicht mehr wie Code-Golf, sondern eher wie gewöhnlicher TypeScript – nur auf der Typenebene.
Wenn ein Typfehler in eine Flut von Bedingungen ausartet, wenden Sie die Methode des Teilens an: Ersetzen Sie vorübergehend einen abgeleiteten Typ durch einen konkreten Objekttyp oder infer R durch eine explizite Anmerkung, bis der fehlerhafte Pfad klar wird. Debuggen auf Typenebene bleibt weiterhin Debuggen; derselbe Ansatz des Teilen und Beherrschen gilt weiterhin.
Man sollte benannte Aliase vor riesigen Einzeileneinträgen bevorzugen, damit zukünftige Leser die Absicht erkennen können. Exportieren Sie nur die Hilfsfunktionen, die Ihre Anwendung tatsächlich wiederverwendet; lassen Sie spekulative Tools vorerst in einer Sandbox, bis ein weiterer Aufrufortpunkt vorhanden ist. So bleibt die Typoberfläche genauso absichtsvoll gestaltet wie die Laufzeit-API.