Was sind fortgeschrittene TypeScript-Typen?
Fortgeschrittene Typen sind Werkzeuge, mit denen Sie Typen auf der Typ-Ebene transformieren, kombinieren und manipulieren können. Anstatt für jede Variation manuell parallele Typen zu definieren, leiten Sie diese automatisch aus bestehenden ab.
Während die Grundlagen Ihnen Typen liefern und Generics es ermöglichen, wiederverwendbaren Code zu schreiben, erlauben Ihnen fortgeschrittene Typen den Aufbau eines Typsystems, das Ihre Daten präzise beschreibt – und so Fehler abfängt, die einfachere Ansätze übersehen würden.
Die drei Säulen sind Utility Types (integrierte Transformationen), Conditional Types (Logik auf Typ-Ebene) und Mapped Types (Transformation von Eigenschaften). Zusammen bilden sie eine kleine Sprache für die Arbeit mit Typen.
Utility Types
TypeScript liefert integrierte Typen mit, mit denen andere Typen modifiziert werden können:
Partial und Required
interface User {
id: number;
name: string;
email: string;
}
// All properties optional
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string; }
// All properties required
type StrictUser = Required<Partial<User>>;
// { id: number; name: string; email: string; }
Pick und Omit
// Extract specific properties
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string; }
// Remove specific properties
type UserWithoutEmail = Omit<User, "email">;
// { id: number; name: string; }
Record
// Create object type with specific keys and values
type Scores = Record<string, number>;
// { [key: string]: number }
const scores: Scores = { math: 95, science: 87 };
// With union keys
type StatusMap = Record<"idle" | "loading" | "done", string>;
Extract und Exclude
type Status = "idle" | "loading" | "success" | "error";
// Keep only matching members
type ActiveStatus = Extract<Status, "loading" | "success">;
// "loading" | "success"
// Remove matching members
type InactiveStatus = Exclude<Status, "loading" | "success">;
// "idle" | "error"
ReturnType und Parameters
function createUser(name: string, age: number) {
return { name, age, id: Date.now() };
}
type User = ReturnType<typeof createUser>;
// { name: string; age: number; id: number }
type Args = Parameters<typeof createUser>;
// [string, number]
NonNullable
type Maybe = string | null | undefined;
type Definite = NonNullable<Maybe>;
// string
Mapped Types
Mapped Types iterieren über die Keys eines bestehenden Typs und transformieren jede Eigenschaft:
type ReadOnly<T> = {
readonly [K in keyof T]: T[K];
};
type MutableUser = ReadOnly<User>;
// All properties are readonly
Modifier hinzufügen
// Add optional
type Optional<T> = {
[K in keyof T]?: T[K];
};
// Remove optional
type Concrete<T> = {
[K in keyof T]-?: T[K];
};
// Remove readonly
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
Die Operatoren - und + entfernen oder fügen Modifier während des Mappings hinzu.
Key-Remapping mit as
TypeScript 4.1+ ermöglicht es, Keys während des Mappings neu zuzuweisen (Remapping):
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string; getEmail: () => string; }
Conditional Types
Conditional Types ermöglichen Logik auf Typ-Ebene:
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
Das infer-Keyword
infer extrahiert einen Typ aus einem anderen Typ:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = () => string;
type Result = ReturnType<Fn>; // string
Praktische Beispiele
// Extract array element type
type ElementOf<T> = T extends (infer E)[] ? E : never;
type Num = ElementOf<number[]>; // number
// Unwrap promises
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type Deep = Awaited<Promise<Promise<string>>>; // string
// Extract object property types
type ValuesOf<T> = T[keyof T];
type UserValues = ValuesOf<User>; // number | string
Distributive Conditional Types
Wenn ein Conditional Type über eine Union verteilt wird, wird jedes Mitglied einzeln geprüft:
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[] — not (string | number)[]
Dies ist nützlich für Typen wie Extract und Exclude, die Union-Mitglieder filtern.
Template Literal Types
Template Literal Types ermöglichen die Manipulation von Strings auf Typ-Ebene:
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">; // "onClick"
type FocusEvent = EventName<"focus">; // "onFocus"
Mit Mapped Types
type EventHandlers<T extends string> = {
[K in T as `on${Capitalize<K>}`]: () => void;
};
type MouseEvents = EventHandlers<"click" | "hover" | "focus">;
// { onClick: () => void; onHover: () => void; onFocus: () => void; }
String Pattern Matching
type CSSProperty = `${string}-${string}`;
type ValidCSS = CSSProperty; // any string with a hyphen
Discriminated Unions
Discriminated Unions sind das wichtigste fortgeschrittene Pattern zur Modellierung von Zuständen:
type RequestState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string };
function handleState(state: RequestState<User>) {
switch (state.status) {
case "idle":
return "Ready";
case "loading":
return "Loading...";
case "success":
return state.data.name; // TypeScript knows data exists
case "error":
return state.error; // TypeScript knows error exists
}
}
Die Eigenschaft status ist der Diskriminator. TypeScript verfeinert den Typ in jedem Branch automatisch.
Branded Types
Branded Types verhindern das Vermischen von Typen, die dieselbe Struktur aufweisen:
type UserId = string & { readonly __brand: unique symbol };
type OrderId = string & { readonly __brand: unique symbol };
function branded<T extends string>(value: T): T & { readonly __brand: unique symbol } {
return value as any;
}
const userId = branded<UserId>("user-123");
const orderId = branded<OrderId>("order-456");
function getUser(id: UserId) { /* ... */ }
getUser(userId); // OK
getUser(orderId); // Error: OrderId is not assignable to UserId
Best Practices
- Verwende Utility Types anstatt Typdefinitionen manuell zu duplizieren.
- Bevorzuge Discriminated Unions gegenüber Type Assertions für die Modellierung von Zuständen.
- Erstelle Typen aus kleinen, kombinierbaren Bausteinen anstatt aus großen, monolithischen Definitionen.
- Setze Conditional Types sparsam ein – wenn ein Typ schwer lesbar ist, vereinfache die API.
- Nutze
infer, um Typen aus Libraries zu extrahieren, anstatt Rückgabewerte manuell zu typisieren. - Behalte die Source of Truth an einem Ort und leite alles Weitere davon ab.
- Verwende Branded Types, wenn du nominales Typing für IDs oder ähnliche Werte benötigst.
- Betrachte komplexe Typen als letzten Ausweg – einfache Typen sind leichter zu warten.
Häufige Fehler
- Over-Engineering von Typen, obwohl einfachere Lösungen ausreichen würden.
- Erstellung tief verschachtelter Conditional Types, die unmöglich zu debuggen sind.
- Verwendung von
asAssertions anstelle von korrektem Type Narrowing. - Nichtnutzung von Utility Types, was zu duplizierten Typdefinitionen führt.
- Typen zu komplex gestalten, im Verhältnis zum Nutzen, den sie bieten.
- Vergessen, dass Typen zur Laufzeit entfernt werden – es gibt kein Runtime Type Checking.
- Komplexe Typen nicht mit Kommentaren dokumentieren, die die Absicht erklären.
Was du als Nächstes lernen solltest
Du verstehst nun fortgeschrittene Typen: Utility Types, Conditional Types, Mapped Types und Template Literals. Der nächste logische Schritt ist TypeScript für die Grundlagen – Annotations, Inference, Interfaces und Generics. Such dir ein Projekt aus, refactore einige Typen und lass die Praxis deine Kenntnisse vertiefen.