Einleitung: Die Magie, die in den Typen verborgen ist
\n\nStellen Sie sich vor, Sie schreiben nicht einfach nur Code, sondern eine Reihe von Anweisungen für den Compiler selbst, der auf der Grundlage Ihrer Regeln sichere, selbst dokumentierende Konstrukte generiert. Das ist Metaprogrammierung in TypeScript – die Fähigkeit, Code zu schreiben, der auf Typebene arbeitet, andere Typen während der Kompilierung analysiert und transformiert.
\n\nViele Entwickler bleiben bei grundlegenden Generics und Interfaces stehen, ohne zu ahnen, welch mächtiges Arsenal sich hinter den Schlüsselwörtern infer, extends und bedingten Konstrukten verbirgt. In diesem Artikel werden wir fortgeschrittene Techniken der Metaprogrammierung in TypeScript behandeln, die Sie von einem einfachen Benutzer der Sprache zu ihrem wahren Architekten machen.
1. Bedingte Typen: if auf Typebene
\n\nBedingte Typen sind die Grundlage jeder Type-Level-Programmierung. Sie funktionieren wie ein ternärer Operator, jedoch auf der Ebene des Typsystems:
\n\ntype IsString<T> = T extends string ? "yes" : "no";\n\ntype Test1 = IsString<"hello">; // "yes"\ntype Test2 = IsString<42>; // "no"\n\n\nDie wahre Magie beginnt jedoch, wenn wir sie mit Generics kombinieren. Erstellen wir zum Beispiel einen Typ, der nur die String-Schlüssel eines Objekts extrahiert:
\n\ntype StringKeys<T> = {\n [K in keyof T]: T[K] extends string ? K : never\n}[keyof T];\n\ninterface User {\n name: string;\n age: number;\n email: string;\n}\n\ntype UserStringKeys = StringKeys<User>; // "name" | "email"\n\n\nDieses Beispiel zeigt, wie fortgeschrittene Typen in TypeScript es ermöglichen, Datenstrukturen bereits zur Kompilierungszeit zu filtern und zu transformieren. Sie programmieren buchstäblich eine Logik, die ausgeführt wird, bevor der Code zur Laufzeit gelangt.
\n\n2. Infer: Typen aus anderen Typen extrahieren
\n\nDas Schlüsselwort infer ist wohl das mächtigste Werkzeug im Arsenal der Metaprogrammierung in TypeScript. Es erlaubt Ihnen, "in einen Typ hineinzuschauen" und seine Bestandteile zu extrahieren. Betrachten wir ein klassisches Beispiel – das Abrufen des Rückgabetyps einer Funktion:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;\n\ntype Fn = (x: number) => string;\ntype FnReturn = ReturnType<Fn>; // string\n\n\nAber infer kann noch viel mehr. Extrahieren wir den Typ des ersten Arguments einer Funktion:
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;\n\ntype Handler = (id: number, name: string) => void;\ntype HandlerFirstArg = FirstArg<Handler>; // number\n\n\nUnd wie wäre es mit einer rekursiven Analyse von Promises? Erstellen wir einen Typ, der alle verschachtelten Promise "entfaltet":
\n\ntype UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;\n\ntype DeepPromise = Promise<Promise<Promise<number>>>;\ntype Unwrapped = UnwrapPromise<DeepPromise>; // number\n\n\nBeachten Sie die Rekursion – dies ist eine der elegantesten fortgeschrittenen Techniken in TypeScript, die es ermöglicht, unendlich verschachtelte Strukturen zu verarbeiten.
\n\n3. Mapped types: Transformation von Objekten
\n\nMapped types ermöglichen es, neue Typen auf der Grundlage vorhandener Typen zu erstellen, indem deren Schlüssel durchlaufen werden. Dies ist ein unverzichtbares Werkzeug zur Erstellung generischer Hilfsfunktionen:
\n\ntype Readonly<T> = {\n readonly [K in keyof T]: T[K];\n};\n\ntype Partial<T> = {\n [K in keyof T]?: T[K];\n};\n\n\nAber wir können noch weiter gehen. Erstellen wir einen Typ, der alle Eigenschaften optional macht, jedoch nur, wenn es sich um Strings handelt:
\n\ntype PartialStringProps<T> = {\n [K in keyof T]: T[K] extends string ? T[K] | undefined : T[K];\n};\n\ninterface Config {\n nam