Sébastien TIMONER
Experte für Webentwicklung und technisches Teammanagement, ich spezialisiere mich auf die Erstellung und Optimierung leistungsstarker digitaler Lösungen. Mit fundierter Expertise in modernen Technologien wie React.js, Node.js, TypeScript, Symfony, Docker und FrankenPHP stelle ich den Erfolg komplexer SaaS-Projekte von der Konzeption bis zur Produktion für Unternehmen verschiedener Branchen sicher.
TypeScript Utility Types sind vordefinierte generische Typen, die Typ-Transformationen erleichtern. Sie ermöglichen es, aus bestehenden Typen neue Typen zu erstellen, zum Beispiel indem bestimmte Eigenschaften optional gemacht oder der Rückgabetyp einer Funktion extrahiert wird. Dieser Leitfaden in drei Stufen (Anfänger, Fortgeschritten, Experte) stellt alle Standard-Utility-Types von TypeScript vor (wie Partial, Required, Readonly, Pick, Omit, Record, Exclude, Extract, NonNullable, Parameters, ReturnType, InstanceType, ThisType usw.), jeweils mit einer Erklärung ihres Nutzens, einem Beispiel in TypeScript und einem Vergleich mit einem äquivalenten Ansatz in Zod, einer Laufzeit-Validierungsbibliothek. Außerdem werden die philosophischen Unterschiede zwischen TypeScript Utility Types (statisches Typsystem) und Zod-Schemas (Laufzeitvalidierung) diskutiert.
Um die Codebeispiele auszuprobieren, kannst du ein minimales TypeScript-Projekt mit Bun initialisieren (ein extrem schneller JavaScript-Runner, der auch als Paketmanager dient). Stelle sicher, dass Bun installiert ist, und führe dann in einem leeren Ordner aus:
shell
Bun erstellt eine Standard-package.json und tsconfig.json. Du kannst dann eine TypeScript-Datei mit bun run file.ts ausführen (Bun transpiliert TypeScript on the fly).
Jetzt können wir Utility Types erkunden!
TypeScript: Der Utility Type Partial<T> erstellt aus einem bestehenden Typ T einen Typ, bei dem alle Eigenschaften optional sind. Das heißt, jede Eigenschaft von T wird optional (es wird ein ? hinzugefügt). So kannst du partielle Objekte dieses Typs darstellen. Beispiel: Wenn du einen User-Typ mit Pflichtfeldern hast, erlaubt Partial<User> eine Version, bei der diese Felder weggelassen werden können:
typescript
Hier hat Partial<User> den Typ { name?: string; age?: number }. Das ermöglicht es zum Beispiel, eine partielle Update-Funktion zu implementieren, ohne alle Felder zu verlangen:
typescript
Du rufst updateUser(existingUser, { age: 26 }) mit einem partiellen Objekt auf – TypeScript garantiert statisch, dass fieldsToUpdate nur gültige Schlüssel von User enthält.
Äquivalent mit Zod: Zod bietet eine äquivalente Methode, um ein Objektschema partiell zu machen. Die Methode .partial() auf einem z.object-Schema macht alle Schlüssel optional, ähnlich wie Partial in TypeScript. Beispiel:
typescript
Hier ist PartialUserSchema ein Schema, das Objekte mit nur einer Teilmenge der Schlüssel validiert (name und age werden optional). Du kannst es so testen:
typescript
Zod erlaubt es auch, nur bestimmte Eigenschaften optional zu machen, indem man ein Objekt als Parameter übergibt (UserSchema.partial({ age: true }) macht nur age optional). Beachte, dass .partial() nur auf der ersten Ebene wirkt; Zod bietet auch .deepPartial(), um verschachtelte Felder rekursiv optional zu machen.
TypeScript: Der Utility Type Required<T> ist das Gegenteil von Partial: Er erstellt aus T einen Typ, bei dem alle Eigenschaften Pflichtfelder sind (nicht optional). Das ist nützlich, um einen Typ zu "verfestigen", dessen Eigenschaften optional waren. Beispiel:
typescript
In diesem Beispiel ist Required<Settings> äquivalent zu { theme: string; fontSize: number } – das Weglassen einer Eigenschaft wird zum Kompilierfehler.
Äquivalent mit Zod: Für ein Zod-Schema gibt es die Methode .required(), die ein Objektschema so transformiert, dass alle Eigenschaften Pflichtfelder werden (im Gegensatz zu .partial(), das sie optional macht). Das ist besonders nützlich, wenn man zuerst ein partielles Schema definiert hat und dann die strikte Version möchte. Beispiel:
typescript
Hier verlangt StrictUserSchema, dass name und age vorhanden sind. Du kannst auch gezielt bestimmte Eigenschaften angeben: BaseUserSchema.required({ age: true }) macht nur age zur Pflicht (und lässt name optional).
Beachte, dass Required<T> zur Kompilierzeit wirkt (im ausgegebenen JavaScript-Code existiert es nicht mehr), während schema.required() von Zod zur Laufzeit die Anwesenheit der Schlüssel prüft.
TypeScript: Der Utility Type Readonly<T> wandelt einen Typ T so um, dass seine Eigenschaften schreibgeschützt (unveränderlich) sind. Jeder Versuch, diese Eigenschaften neu zuzuweisen, wird beim Kompilieren als Fehler gemeldet. Beispiel:
typescript
Hier ist todo vom Typ Readonly<Todo>, daher können seine Felder nach der Initialisierung nicht mehr verändert werden. Das entspricht oft der Verwendung von Object.freeze in JavaScript, aber TypeScript behandelt dies nur auf Typ-Ebene (es verhindert keine Mutation zur Laufzeit, sondern warnt nur beim Kompilieren).
Äquivalent mit Zod: Zod bietet eine interessante Funktion für Unveränderlichkeit. Die Methode .readonly() auf einem Schema gibt ein neues Schema zurück, das beim Parsen Object.freeze() auf das Ergebnis anwendet und dessen statischer Typ als Readonly<...> markiert ist. Zod kann also das validierte Objekt einfrieren und sicherstellen, dass es einem readonly-Typ entspricht. Beispiel:
typescript
Hier friert FrozenUserSchema das von parse zurückgegebene Objekt ein. Wenn du versuchst, frozenUser zu verändern, wird zur Laufzeit eine Ausnahme ausgelöst, weil das Objekt unveränderlich ist. Zod geht also weiter als TypeScript: Während Readonly<T> rein statisch ist (Eigenschaften könnten in JS verändert werden, wenn man das Typing umgeht), sorgt das Zod .readonly()-Schema für echte Unveränderlichkeit zur Laufzeit und liefert zusätzlich den entsprechenden Readonly<...>-Typ.
TypeScript: Pick<T, Keys> erstellt aus T einen neuen Typ, der nur die Eigenschaften enthält, die durch Keys angegeben sind (typischerweise eine Union von String-Literalen als Eigenschaftsnamen). Das ist nützlich, um aus einem bestehenden Typ einen eingeschränkten Typ zu machen. Beispiel:
typescript
Hier enthält TodoPreview nur title und completed aus FullTodo. Jede nicht aufgeführte Eigenschaft (z.B. description) wird im resultierenden Typ ausgeschlossen.
Äquivalent mit Zod: Zod-Objektschemas haben ebenfalls eine .pick()-Methode, die ein neues Schema mit nur den angegebenen Schlüsseln erstellt, ähnlich wie Pick in TypeScript:
typescript
Du übergibst ein Objekt mit den zu behaltenden Schlüsseln auf true gesetzt. TodoPreviewSchema validiert nur Objekte mit title und completed (andere Eigenschaften von FullTodo werden vom Parser standardmäßig ignoriert). Zum Beispiel gibt TodoPreviewSchema.parse({ title: "Clean", completed: false, description: "..." }) { title: "...", completed: false } zurück, wobei description entfernt wurde (weil Zod standardmäßig nicht spezifizierte Schlüssel ignoriert; dieses Verhalten kann mit .strict() oder .passthrough() angepasst werden).
Ebenso bietet Zod .omit() für den gegenteiligen Effekt.
TypeScript: Omit<T, Keys> erstellt aus T einen neuen Typ, bei dem bestimmte Eigenschaften (Keys) entfernt werden. Es ist das Gegenstück zu Pick (tatsächlich wird Omit<T, K> oft als Pick<T, Exclude<keyof T, K>> implementiert). Beispiel:
typescript
Hier enthält TaskMeta nur id und title. Die Eigenschaften done und dueDate wurden ausgeschlossen.
Äquivalent mit Zod: Die .omit()-Methode von Zod erzeugt ein Objektschema ohne die angegebenen Schlüssel. Beispiel:
typescript
TaskMetaSchema validiert Objekte, die nur id und title enthalten. Jeder ausgelassene Schlüssel (done, dueDate) wird ignoriert, wenn er im Input vorhanden ist. Pick und Omit erlauben es, ein Zod-Schema auf eine Teilmenge von Feldern zu projizieren, genau wie ihre TypeScript-Äquivalente auf Typ-Ebene.
TypeScript: Record<Keys, Type> erstellt einen Objekttyp, dessen Schlüssel vom Typ Keys sind (typischerweise eine Union von String-Literalen oder ein allgemeiner string/number-Typ) und dessen Werte vom Typ Type sind. Damit kannst du z.B. ein Objekt beschreiben, das nach Bezeichnern indiziert ist. Ein klassisches Beispiel:
typescript
Hier stellt Record<CatName, CatInfo> sicher, dass das cats-Objekt genau die Schlüssel "miffy", "boris", "mordred" hat, jeweils mit einem CatInfo verknüpft. Record ist sehr nützlich, um Objekte als Wörterbücher oder Maps zu typisieren.
Äquivalent mit Zod: Zod bietet das Schema z.record(keySchema, valueSchema), um solche Objekte zu validieren. Du kannst es mit einem Schlüssel-Schema (z.B. z.string() oder ein Literal-Schema) und einem Wert-Schema verwenden. Beispiel:
typescript
Hier validiert AgeMapSchema jeden Schlüssel, solange er ein String ist (standardmäßig nimmt z.record ohne erstes Argument string an). Wenn du eine endliche Menge von Schlüsseln hast, ist es oft besser, z.object({ ... }) mit explizit benannten Schlüsseln oder z.enum([...]) zu verwenden, um die Schlüssel einzuschränken. Tatsächlich erlaubt Zod ein strengeres Schlüssel-Schema: Du könntest const CatNameSchema = z.enum(["miffy","boris","mordred"]) definieren und dann z.record(CatNameSchema, CatInfoSchema) verwenden – beachte aber, dass zur Laufzeit JavaScript-Objektschlüssel immer Strings sind. Auch ein numerischer Schlüssel wird zur Laufzeit in einen String umgewandelt (z.B. wird ein Schlüssel 1 zu "1" in einem JS-Objekt). Zod berücksichtigt das und erlaubt kein reines numerisches Schlüssel-Schema ohne Umwandlung in String.
Zusammengefasst: Record<Keys, Type> (TypeScript) und z.record(keySchema, valueSchema) (Zod) erlauben beide die Typisierung von Wörterbüchern. Ersteres wirkt auf Typ-System-Ebene, letzteres validiert die Objektstruktur zur Laufzeit.
TypeScript: Exclude<U, E> erstellt einen Typ, indem aus der Union U alle Mitglieder entfernt werden, die auf den Typ E zuweisbar sind. Anders gesagt: Du schließt bestimmte Typen aus einer Union aus. Beispiel:
typescript
Hier entfernt Exclude<Role, "admin"> das Literal "admin" aus der Union, übrig bleiben "user" | "guest". Ein weiteres Beispiel: Exclude<string | number | boolean, string> ergibt number | boolean (alles, was auf string zuweisbar ist, wird entfernt). Das ist nützlich, um eine Union zu verfeinern und unerwünschte Fälle auszuschließen.
Äquivalent mit Zod: Um die Möglichkeiten einer Union zur Laufzeit einzuschränken, definierst du typischerweise ein Schema, das die auszuschließenden Fälle nicht enthält. Wenn du z.B. ein RoleSchema = z.enum(["admin","user","guest"]) hast, kannst du eine Teilmenge erstellen, indem du "admin" nicht einschließt. Zod bietet für Enums die Methode .exclude([…]), die ein neues Enum-Schema ohne die angegebenen Werte erzeugt. Beispiel:
typescript
Dieses NonAdminSchema entspricht genau dem Typ Exclude<Role, "admin">, stellt aber auch die Validierung zur Laufzeit sicher. Intern entfernt RoleSchema.exclude(["admin"]) einfach "admin" aus der Liste der erlaubten Literale. Ebenso bietet Zod .extract([...]) für die komplementäre Operation.
TypeScript: Extract<T, U> erstellt einen Typ, indem aus T nur die Mitglieder behalten werden, die auf U zuweisbar sind. Das ist grob das Gegenteil von Exclude. Beispiel:
typescript
Ein weiteres Beispiel: Extract<"a" | "b" | "c", "a" | "f"> ergibt "a" (weil nur "a" in beiden vorkommt). Dieser Utility Type wird also verwendet, um eine Union zu filtern und nur einen bestimmten Subtyp zu extrahieren.
Äquivalent mit Zod: Wie bei Exclude erlaubt Zod, zur Laufzeit ein Schema zu erhalten, das einer Teilmenge einer Enumeration entspricht, über .extract([...]). Beispiel mit Rollen: Um nur die eingeschränkten Rollen "user" und "guest" aus einem vollständigen RoleSchema zu extrahieren:
typescript
Hier entspricht LimitedRolesSchema dem Typ Extract<Role, "user" | "guest">. Praktisch könntest du das gleiche Ergebnis auch direkt mit z.enum(["user","guest"]) erreichen, aber .extract ist praktisch, um ein Schema aus einem bestehenden Enum abzuleiten. Beachte, dass .exclude und .extract von Zod auf Enum-Schemas (z.enum oder z.nativeEnum) angewendet werden, nicht auf beliebige Unions komplexer Typen. Für letztere baust du das gewünschte neue Union-Schema meist manuell.
TypeScript: NonNullable<T> erstellt einen Typ, indem null und undefined aus T entfernt werden. Es ist in TypeScript üblich, Union-Typen mit diesen "nullish"-Werten zu haben und einen Typ zu wollen, der davon bereinigt ist. Beispiel:
typescript
Hier enthält DefinitelyString nur string (weder null noch undefined). Dieser Utility Type wird oft in Kombination mit Präsenzbedingungen verwendet (if (value != null) { ... }), um dem Compiler zu helfen, Typen zu verfeinern.
Äquivalent mit Zod: Bei Zod akzeptieren Schemas standardmäßig weder undefined noch null, es sei denn, du gibst es explizit an. Ein z.string()-Schema akzeptiert also nur einen String, kein undefined. Um diese Werte zuzulassen, verwendest du explizit z.string().optional() (erlaubt undefined) oder z.string().nullable() (erlaubt null) oder .nullish() (erlaubt beides). Das Äquivalent zu einem NonNullable-Typ erhältst du also einfach, indem du null/undefined nicht im Schema zulässt. Wenn du von einem großzügigeren Schema ausgehst und es strikter machen willst, kannst du verschiedene Techniken kombinieren: Die Methode .unwrap() erlaubt es, das zugrundeliegende Schema aus einem optionalen oder nullable-Schema zu extrahieren.
typescript
Hier gibt OptionalName.unwrap() das ursprüngliche z.string()-Schema zurück. So kannst du das optionale oder nullable-Charakteristikum wieder "entfernen". Ein anderer Ansatz wäre ein Custom Refinement, um null/undefined abzulehnen, aber das wäre redundant, da Zod-Schemas dies ohnehin standardmäßig tun.
TypeScript: Parameters<T> extrahiert die Parametertypen eines Funktionstyps T als Tupel. Das ist nützlich, um mit Funktionssignaturen zu arbeiten, besonders wenn du eine Funktion erstellen willst, die eine andere Funktion umschließt. Beispiel:
typescript
Hier extrahiert Parameters<typeof greet> das Tupel [string, number], das die Parameter von greet repräsentiert. So kann safeGreet genau die gleichen Parameter wie greet akzeptieren.
Äquivalent mit Zod: Zod bietet z.function(), um Funktionsschemas zu definieren, mit Methoden zur Angabe von Parametertypen und Rückgabetyp. Es gibt zwar kein direktes Äquivalent zu Parameters<T>, aber du kannst ein Funktionsschema definieren und die Parametertypen mit TypeScripts eigenem Parameters-Utility auf dem abgeleiteten Typ extrahieren:
typescript
Hier definiert GreetSchema ein Funktionsschema, das einen String und eine Zahl nimmt und einen String zurückgibt. Die Methode .implement() erstellt eine Wrapper-Funktion, die Eingaben und Ausgaben gemäß dem Schema validiert. Wir können die Parametertypen mit Parameters<GreetFn> extrahieren. Dieser Ansatz kombiniert Laufzeitvalidierung mit statischer Typisierung.
TypeScript: ReturnType<T> extrahiert den Rückgabetyp eines Funktionstyps T. Das ist nützlich, wenn du mit dem Ergebnis einer Funktion arbeiten willst, ohne den Typ explizit neu zu definieren. Beispiel:
typescript
Hier extrahiert ReturnType<typeof createUser> den Objekttyp, der von createUser zurückgegeben wird. So können wir den User-Typ definieren, ohne die Struktur zu duplizieren.
Äquivalent mit Zod: Wie bei Parameters gibt es kein direktes Zod-Äquivalent zu ReturnType, aber du kannst das Funktionsschema von Zod mit TypeScripts ReturnType-Utility kombinieren:
typescript
Hier definieren wir ein Funktionsschema mit CreateUserSchema und extrahieren dann den Rückgabetyp auf zwei Arten: mit TypeScripts ReturnType auf dem abgeleiteten Funktionstyp oder mit Zods .returnType(), das das Schema für den Rückgabewert liefert. Beide Ansätze ergeben den User-Typ.
TypeScript: InstanceType<T> extrahiert den Instanztyp eines Konstruktortyps T. Das ist nützlich, wenn du eine Klassenreferenz hast und mit Instanzen dieser Klasse arbeiten willst. Beispiel:
typescript
Hier gibt InstanceType<typeof Point> uns den Typ einer Point-Instanz, der ihre Eigenschaften und Methoden enthält.
Äquivalent mit Zod: Zod hat kein direktes Äquivalent für Klasseninstanzen, da es sich auf Datenvalidierung und nicht auf Klassenverhalten konzentriert. Du kannst aber ein Schema für die Form von Klasseninstanzen definieren:
typescript
Dieser Ansatz definiert ein Schema, das der Form einer Point-Instanz entspricht, aber nicht die vollständige Klassenbeziehung abbildet. Für komplexere Klassenvalidierung musst du ggf. Zod mit class-validator oder ähnlichen Bibliotheken kombinieren.
TypeScript Utility Types und Zod-Schemas dienen beide der Datensicherheit, aber auf unterschiedlichen Ebenen: einer zur Kompilierzeit, der andere zur Laufzeit.
Zusammengefasst:
Philosophisch gesehen drücken Utility Types Transformationen oder Einschränkungen auf Typ-System-Ebene aus (sie helfen dem Entwickler, haben aber keinen direkten Einfluss auf den ausgegebenen Code), während Zod zur Laufzeit Verträge bereitstellt (sie ermöglichen die konkrete Implementierung von Datenvalidierungen und -transformationen zur Ausführung). Beide Ansätze adressieren unterschiedliche, aber verwandte Bedürfnisse: die Robustheit des Codes. Zod ist zudem so konzipiert, dass es sich harmonisch in TypeScript integriert: Es leitet automatisch statische Typen aus deinen Schemas ab, sodass du keine Definitionen duplizieren musst. Oft kannst du ein Zod-Schema (für die Validierung) definieren und mit z.infer den entsprechenden statischen TypeScript-Typ ableiten, statt ein TypeScript-Interface und ein separates Schema zu definieren – so hast du eine einzige Quelle der Wahrheit für deine Datentypen.
Fazit: Wer TypeScript Utility Types kennt, kann das Typsystem optimal nutzen, um sicheren und ausdrucksstarken Code zu schreiben. Wer Zod beherrscht, hat die Werkzeuge, um diese Typverträge zur Laufzeit durchzusetzen. Zusammen stärken sie die Zuverlässigkeit deiner TypeScript-Anwendungen erheblich: TypeScript schützt vor Programmierfehlern, Zod schützt deine Anwendung vor unerwarteten Daten von außen. Definiere deine Typen, validiere deine Daten und programmiere mit ruhigem Gewissen 🎉!