Nutzen Sie die eingebauten Hilfsdatentypen von TypeScript für saubereren Code
Erfahren Sie, wie die Hilfstypen von TypeScript wie Partial, Pick, Omit und Record doppelte Schnittstellen beseitigen und die Typdefinitionen automatisch synchron halten.
Hören Sie auf, Schnittstellen zu kopieren und einzufügen: Schreiben Sie mit den integrierten Typumwandlern von TypeScript saubereren, fehlerresistenten Code.
Eher früher oder später stößt jeder TypeScript-Entwickler auf dieselbe Herausforderung. Sie entwerfen eine saubere User-Schnittstelle mit acht Feldern. Ein paar Minuten später benötigen Sie ein Bearbeitungsformular, bei dem nur zwei Felder tatsächlich erforderlich sind, also erstellen Sie eine UpdateUserInput-Schnittstelle. Danach kommt eine API-Antwort, bei der sensible Felder versteckt werden müssen, weshalb Sie auch eine PublicUser-Schnittstelle hinzufügen. Bevor man es merkt, besteht ein großer Teil Ihrer Codebasis aus nahezu identischen Schnittstellen, die im Laufe des Projektwachstums stillschweigend aus dem Gleichgewicht geraten.
Jedes Mal, wenn sich das zugrundeliegende Schema ändert, müssen Sie mehrere Stellen gleichzeitig finden und aktualisieren. Vergessen Sie eine davon, haben Sie gerade einen Fehler verursacht, der später in der Produktion zum Vorschein kommt.
TypeScript löst dieses Problem mit einem eingebauten Werkzeugkasten namens Utility Types. Man kann sie sich als Funktionen vorstellen, die mit Typen statt mit Werten arbeiten: Man gibt einen vorhandenen Typ ein, wendet eine Transformation regel an und erhält anschließend einen völlig neuen Typ. Dadurch kann man eine einzige, verbindliche Definition beibehalten und die gesamte repetitive Umformungsarbeit dem Compiler überlassen.
Unten sind die Utility Types aufgeführt, die man ständig verwenden sollte – erläutert in einfachen Worten zusammen mit realistischen Beispielen, die man sofort anpassen kann.
1. Umwandlung der Eigenschaftsstrenge
Diese drei Utility Types steuern, ob Felder optional, erforderlich oder unveränderlich sind.
Partial<Type>
In einfachen Worten: "Mache jede Eigenschaft optional."
Immer wenn Sie etwas wie einen PATCH-Endpunkt erstellen oder den Zustand eines Anwendungsteils aktualisieren, möchten Sie nicht, dass der Aufrufer das gesamte Objekt übermittelt. Partial<T> durchläuft jedes Feld einer Schnittstelle und fügt zu jedem ein ? hinzu.
interface UserProfile {
id: string;
name: string;
email: string;
avatarUrl: string;
theme: "light" | "dark";
}
// Without Partial, you'd have to rewrite all fields with '?'
type UpdateProfileInput = Partial<UserProfile>;
function updateProfile(userId: string, changes: UpdateProfileInput) {
// Valid: changes can include any subset of UserProfile properties
apiClient.patch(`/users/${userId}`, changes);
}
// Valid call:
updateProfile("usr_101", { theme: "dark" });
Required<Type>
In einfachen Worten: "Entfernen Sie alle optionalen Markierungen, damit nichts übersprungen werden kann."
Required<T> tut das Gegenteil von Partial<T>: Bei einer Schnittstelle, bei der einige Felder optional sind, zwingt es dazu, dass alle vorhanden sind. Dies ist nützlich, wenn Sie Standardeinstellungen einfügen und sicherstellen müssen, dass nichts fehlt.
interface AppConfig {
apiUrl: string;
timeout?: number;
retries?: number;
enableLogging?: boolean;
}
// Ensure the internal runner has every single setting resolved
type ResolvedConfig = Required<AppConfig>;
const defaultConfig: ResolvedConfig = {
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3,
enableLogging: true,
};
function initializeApp(userOptions: AppConfig): ResolvedConfig {
return { ...defaultConfig, ...userOptions };
}
Readonly<Type>
Einfach ausgedrückt: "Freezen Sie alle Eigenschaften, damit sie nicht neu zugewiesen werden können."
Wenn Sie vor versehentlichen Änderungen schützen möchten – denken Sie an Konfigurationswerte, Eigenschaften von Komponenten oder gemeinsamen Anwendungsstatus – markieren Sie mit Readonly<T> alle Eigenschaften als readonly. Jeder Versuch, danach in eine dieser Eigenschaften einzuschreiben, scheitert bereits zur Kompilierzeit.
interface SystemRole {
roleName: string;
permissions: string[];
}
const SuperAdminRole: Readonly<SystemRole> = {
roleName: "SUPER_ADMIN",
permissions: ["read", "write", "delete", "admin"],
};
// Error: Cannot assign to 'roleName' because it is a read-only property.
SuperAdminRole.roleName = "USER";
2. Das Notwendige herausfiltern
Es gibt viele Situationen, in denen Sie nur einen Teil einer Schnittstelle benötigen. Anstatt einen völlig neuen, unverwandten Typ manuell zu erstellen, können Sie direkt Teile aus Ihren vorhandenen Domänenmodellen verwenden.
Pick<Type, Keys>
Einfach ausgedrückt: "Erstellen Sie einen neuen Typ, der ausschließlich aus diesen benannten Schlüsseln des ursprünglichen Types besteht."
Nehmen wir an, Sie haben ein großes Modell, aber ein kleines UI-Widget oder eine leichte Datenbankabfrage benötigen nur einen Namen und ein Avatar. Sie können TypeScript anweisen, ausschließlich diese beiden Felder beizubehalten.
interface Product {
id: string;
sku: string;
name: string;
price: number;
stockQuantity: number;
supplierId: string;
description: string;
}
// Pick only the fields a mini checkout card displays
type CartItemPreview = Pick<Product, "id" | "name" | "price">;
const item: CartItemPreview = {
id: "prod_99",
name: "Wireless Mechanical Keyboard",
price: 129.99,
};
Omit<Type, Keys>
In einfachen Worten: "Nehmen Sie den gesamten ursprünglichen Typ, aber entfernen Sie diese bestimmten Felder."
Omit<T, K> umkehrt die Funktion von Pick. Wenn Sie eine neue Zeile in eine Datenbank einfügen, haben Sie in der Regel alle Felder außer denen, die das System automatisch erzeugt, wie zum Beispiel id oder createdAt.
interface Article {
id: number;
title: string;
slug: string;
content: string;
publishedAt: Date;
viewCount: number;
}
// Strip metadata handled by the database
type CreateArticlePayload = Omit<Article, "id" | "viewCount" | "publishedAt">;
function submitArticle(payload: CreateArticlePayload) {
// TypeScript guarantees no one accidentally submits an 'id' or 'viewCount'
apiClient.post("/articles", payload);
}
3. Dynamische Schlüsselzuordnung
Record<Keys, Type>
Einfach ausgedrückt: ">Erstellen Sie ein Wörterbuch, dessen Schlüssel aus dieser Liste stammen und dessen Werte diese Struktur aufweisen."
Einfache JavaScript-Objekte, die als Suchtabellen verwendet werden, haben oft eine lockere Typisierung, etwa in Form von { [key: string]: any }, was die Typsicherheit untergräbt. Record<Keys, Type> legt hingegen genau fest, welche Schlüssel erlaubt sind und welche Struktur ihre Werte haben müssen.
type SubscriptionTier = "free" | "pro" | "enterprise";
interface TierFeatures {
monthlyQuota: number;
customDomainAllowed: boolean;
supportLevel: "community" | "email" | "dedicated";
}
// Guarantees all three tiers are explicitly handled
const subscriptionPlans: Record<SubscriptionTier, TierFeatures> = {
free: {
monthlyQuota: 1000,
customDomainAllowed: false,
supportLevel: "community",
},
pro: {
monthlyQuota: 50000,
customDomainAllowed: true,
supportLevel: "email",
},
enterprise: {
monthlyQuota: 1000000,
customDomainAllowed: true,
supportLevel: "dedicated",
},
};
Falls später eine neue „Starter“-Ebene zu SubscriptionTier hinzugefügt wird, markiert TypeScript subscriptionPlans sofort und informiert Sie darüber, dass der Eintrag für „Starter“ fehlt.
4. Reduzierung und Einschränkung von Union-Typen
Die oben genannten Hilfsfunktionen formen die Schlüssel von Objekten um. Das nächste Paar hingegen arbeitet mit Union-Typen, wie zum Beispiel "apple" | "banana" | "orange".
Exclude<UnionType, ExcludedMembers>
In einfachen Worten: ">Nehmen Sie diese Menge möglicher Werte und entfernen Sie die hier aufgeführten.
type TaskStatus = "draft" | "in_progress" | "review" | "completed" | "archived";
// You can edit tasks in any status except when they are completed or archived
type EditableTaskStatus = Exclude<TaskStatus, "completed" | "archived">;
// Result: "draft" | "in_progress" | "review"
Extract<UnionType, ExtractedMembers>
In einfachen Worten: ">Nehmen Sie diese Menge möglicher Werte und behalten Sie nur diejenigen, die diesen Kriterien entsprechen.
Extract funktioniert in entgegengesetzter Richtung zu Exclude: Es isoliert die Überschneidung zwischen zwei Union-Typen und behält nur die Mitglieder bei, die in beiden vorkommen.
type WindowEvents = "click" | "scroll" | "mousemove" | "keydown" | "keyup";
type PointerEvents = "click" | "mouseenter" | "mouseleave" | "mousemove";
// Keep only the events shared across both categories
type SharedEvents = Extract<WindowEvents, PointerEvents>;
// Result: "click" | "mousemove"
NonNullable<Type>
null und undefined aus einem Typ entfernen.
string | null | undefined typisiert. NonNullable<T> beseitigt diese möglichen leeren Werte, sodass nur die sinnvollen Typen übrig bleiben.
type SearchQuery = string | string[] | null | undefined;
type CleanSearchQuery = NonNullable<SearchQuery>;
// Result: string | string[]
5. Typen aus vorhandenem Code ableiten
ReturnType<FunctionType>
Auf einfache Sprache übersetzt: Es zeigt an, was eine Funktion zurückgibt, sodass man diesen Typ nie wieder manuell eingeben muss.
Für komplexe Fabrikfunktionen, Redux-ähnliche Selektoren oder Drittanbieter-Pakete, die ihre Rückgabestruktur nicht offenlegen, können Sie die genaue Struktur ermitteln, indem Sie auf die Funktion selbst verweisen.
function buildSessionPayload(userId: string, roles: string[]) {
return {
sessionId: crypto.randomUUID(),
authenticatedAt: Date.now(),
expiresAt: Date.now() + 1000 * 60 * 60 * 24,
account: {
id: userId,
primaryRole: roles[0] ?? "guest",
allRoles: roles,
},
};
}
// Automatically inherits the shape of whatever buildSessionPayload returns
type UserSession = ReturnType<typeof buildSessionPayload>;
Falls sich das in buildSessionPayload zurückgegebene Objekt später ändert, erkennt UserSession die Änderung automatisch.
Parameters<FunctionType>
In einfachen Worten: Es erzeugt eine Tupel-Struktur, die alle Argumente auflistet, die eine Funktion entgegennimmt.
Jedes Mal, wenn Sie eine SDK-Methode umhüllen, Argumente weiterleiten oder eine Logging-Schicht um einen Funktionsaufruf bauen, sorgt Parameters<T> dafür, dass die Signatur Ihres Wrapper niemals von der ursprünglichen abweicht.
function triggerAlert(message: string, severity: "low" | "high", code?: number) {
// Internal dispatch logic
}
// Extracts arguments as a tuple: [message: string, severity: "low" | "high", code?: number]
type AlertArgs = Parameters<typeof triggerAlert>;
function logAndAlert(...args: AlertArgs) {
console.log("Dispatching alert:", args[0]);
triggerAlert(...args);
}
Awaited<Type>
Einfach ausgedrückt: Es entpackt eine Promise und gibt den darin enthaltenen Wert frei.
Da viel moderner TypeScript um asynchrone Codeblöcke kreist, ist Awaited<T> unerlässlich, um eine oder mehrere Schichten von verschachtelten Promises abzulösen, bis man den tatsächlich gelösten Wert erreicht.
async function fetchAccountDetails() {
return {
accountId: "acct_8472",
balance: 4250.75,
currency: "USD",
};
}
// Unwraps the Promise<T> returned by the async function
type AccountDetails = Awaited<ReturnType<typeof fetchAccountDetails>>;
// Result: { accountId: string; balance: number; currency: string }
const cachedAccount: AccountDetails = {
accountId: "acct_8472",
balance: 4250.75,
currency: "USD",
};
6. Nutztypen wie Bausteine stapeln
Nutztypen zeigen ihre wahre Stärke, wenn sie kombiniert werden. Da jeder von ihnen einen standardmäßigen TypeScript-Typ ausgibt, kann man das Ergebnis eines Typs direkt in einen anderen einfügen.
Betrachten wir eine Datenschicht, die um eine Customer-Entität aufgebaut ist:
interface Customer {
id: string;
firstName: string;
lastName: string;
email: string;
phoneNumber?: string;
createdAt: Date;
updatedAt: Date;
}
// 1. Creation payload: No auto-generated fields, but firstName, lastName, and email are mandatory
type CreateCustomerDTO = Omit<Customer, "id" | "createdAt" | "updatedAt">;
// 2. Update payload: Omit IDs, and let the user modify ANY valid field optionally
type UpdateCustomerDTO = Partial<Omit<Customer, "id" | "createdAt" | "updatedAt">>;
// 3. Read-only view for UI caches
type ReadonlyCustomer = Readonly<Customer>;
Mit nur wenigen Zeilen haben Sie drei separate, vollständig typgeprüfte Objekte für den Datentransfer erstellt. Sollte die Basisklasse Customer später eine neue Eigenschaft erhalten – sagen wir loyaltyTier – werden alle drei abgeleiteten Typen automatisch aktualisiert, ohne dass manuelle Änderungen erforderlich sind.
Kurzfassung zur schnellen Referenz
Die Behandlung von Typen als manuell kopierte Duplikate macht jede Änderung am Schema zu einer Wartungsbelastung. Hilftypen ermöglichen es Ihnen, Ihre Geschäftsmodelle einmal zu definieren, und der Compiler erzeugt anschließend alle benötigten Variationen aus dieser einzigen Definition. Das Ergebnis sind kompakterer Code, sicherere Refaktorisierungen sowie weniger Zeit für die Abstimmung nahezu identischer Schnittstellen, sodass Sie sich auf die Entwicklung neuer Funktionen konzentrieren können.