TypeScript Utility Types: Partial, Pick, Omit & Record
TypeScript Utility Types mit Beispielen: Partial, Pick, Omit und Record vergleichen, sichere Update-Typen ableiten und typische Laufzeitfallen vermeiden.
TypeScript Utility Types leiten neue Typen aus vorhandenen Typen ab. Partial<T> macht Eigenschaften optional, Pick<T, K> wählt Eigenschaften aus, Omit<T, K> lässt sie im abgeleiteten Typ weg und Record<K, V> beschreibt eine Zuordnung von Schlüsseln zu Werten.
Das spart wiederholte Typdefinitionen. Es verändert jedoch keine JavaScript-Objekte: Wer passwordHash mit Omit aus einem Typ entfernt, hat es damit noch nicht aus einer API-Antwort entfernt. Genau diese Grenze sollte man zusammen mit der Syntax lernen.
Die Beispiele bauen aufeinander auf. Die mit @ts-expect-error markierten Zeilen zeigen bewusst ungültige Zuweisungen; der Compiler prüft dabei, dass an dieser Stelle tatsächlich ein Typfehler entsteht.
Die wichtigsten Utility Types im Vergleich
| Typ | Wirkung auf den Typ | Typischer Einsatz |
|---|---|---|
Partial<T> |
Alle Eigenschaften optional | Ein teilweise ausgefülltes Formular |
Required<T> |
Optionale Eigenschaften werden Pflicht | Eine vervollständigte Konfiguration |
Pick<T, K> |
Nur ausgewählte Eigenschaften behalten | Kleine Ansicht oder Eingabevertrag |
Omit<T, K> |
Benannte Eigenschaften weglassen | Variante eines bekannten internen Typs |
Record<K, V> |
Schlüssel mit einem Werttyp verknüpfen | Beschriftungen für bekannte Statuswerte |
Readonly<T> |
Zuweisung an Eigenschaften verhindern | Schreibgeschützter Zugriff im Typmodell |
Die vollständigen Definitionen stehen im offiziellen TypeScript-Handbuch zu Utility Types. Für die folgenden Beispiele reicht ein gemeinsames Modell:
type User = {
id: number;
name: string;
email: string;
passwordHash: string;
role: "admin" | "user";
profile: {
city: string;
timezone: string;
};
};
Partial: einzelne Felder eines Updates übergeben
Ein Profilformular muss nicht bei jeder Änderung alle Benutzerdaten schicken. Es sollte aber auch nicht plötzlich die Rolle oder den Passwort-Hash bearbeiten dürfen.
Deshalb wählen wir zuerst die vorgesehenen Felder und machen anschließend nur diese optional:
type ProfileChanges = Partial<Pick<User, "name" | "email">>;
const nameChange: ProfileChanges = {
name: "Mina",
};
const emailChange: ProfileChanges = {
email: "mina@example.com",
};
const noChanges: ProfileChanges = {};
// @ts-expect-error: role gehört nicht zu den editierbaren Feldern
const invalidChange: ProfileChanges = { role: "admin" };
Die Kombination dokumentiert einen konkreten Vertrag: Ein Aufrufer darf Name, E-Mail, beides oder keines von beiden übergeben. Ob ein leeres Update fachlich sinnvoll ist, entscheidet weiterhin die Anwendung.
Partial<User> wäre für dieses Formular zu weit gefasst. Es würde auch id, role und passwordHash als optionale Eingabefelder zulassen. Ein kurzer Typ ist noch kein passender Typ.
Partial arbeitet auf der obersten Ebene
Wenn profile vorhanden ist, muss es weiterhin dem vollständigen Profiltyp entsprechen:
const completeProfile: Partial<User> = {
profile: {
city: "Essen",
timezone: "Europe/Berlin",
},
};
// @ts-expect-error: Innerhalb von profile fehlt timezone
const incompleteProfile: Partial<User> = { profile: { city: "Essen" } };
Für ein bewusst verschachteltes Update braucht es einen eigenen Vertrag. Der sollte auch erklären, ob ein inneres Objekt ersetzt oder mit bestehenden Daten zusammengeführt wird. Ein rekursiver „DeepPartial“-Typ beantwortet diese fachliche Frage nicht.
Dieses Verhalten ergibt sich aus den Mapped Types: Ein Modifier wie optional wird auf die Eigenschaften des gerade betrachteten Typs angewendet.
Optional ist nicht immer gleich undefined
Mit exactOptionalPropertyTypes unterscheidet TypeScript zwischen einer fehlenden Eigenschaft und einer vorhandenen Eigenschaft mit dem Wert undefined. Bei name?: string ist dann {} zulässig, { name: undefined } dagegen nicht – außer undefined gehört ausdrücklich zum erlaubten Werttyp. Diese Einstellung wird zusätzlich zu strict aktiviert. Compiler-Option exactOptionalPropertyTypes
Für Update-Schnittstellen lohnt sich diese Unterscheidung: „nicht ändern“, „Wert setzen“ und „Wert löschen“ benötigen eine klare Bedeutung.
Pick vs. Omit: auswählen oder ausschließen?
Beide Typen können zu einer ähnlichen Form führen. Der Unterschied liegt darin, was bei einer späteren Erweiterung des Ausgangstyps passiert.
type PublicProfile = Pick<User, "id" | "name">;
type InternalUserView = Omit<User, "passwordHash">;
const publicProfile: PublicProfile = {
id: 7,
name: "Mina",
};
PublicProfile enthält ausdrücklich nur id und name. InternalUserView enthält alle Eigenschaften des Benutzers außer passwordHash – also auch E-Mail, Rolle und Profil.
| Frage | Pick | Omit |
|---|---|---|
| Was schreibe ich auf? | Die gewünschten Felder | Die ausgeschlossenen Felder |
| Was passiert bei einem neuen Feld in User? | Es wird nicht automatisch übernommen | Es wird übernommen, sofern nicht ausgeschlossen |
| Wann ist das hilfreich? | Enger, bewusst gewählter Vertrag | Interne Variante, die dem Ausgangstyp folgen soll |
Für öffentliche Antworten und bearbeitbare Felder bevorzuge ich deshalb häufig eine explizite Auswahl. Für eine interne Ansicht kann es dagegen gewollt sein, neue Eigenschaften automatisch zu übernehmen. Das ist eine Entwurfsentscheidung, keine pauschale Regel zugunsten des kürzeren Ausdrucks.
Omit entfernt keine Daten zur Laufzeit
Dieses Beispiel ist typkorrekt und trotzdem als Datenfilter ungeeignet:
const storedUser: User = {
id: 7,
name: "Mina",
email: "mina@example.com",
passwordHash: "demo-hash",
role: "user",
profile: {
city: "Essen",
timezone: "Europe/Berlin",
},
};
const typedView: Omit<User, "passwordHash"> = storedUser;
console.log(typedView === storedUser); // true
console.log(JSON.stringify(typedView).includes("passwordHash")); // true
Beide Variablen verweisen auf dasselbe Objekt. Die Typannotation hat keine Eigenschaft gelöscht. TypeScript erlaubt an dieser Stelle die Zuweisung eines vorhandenen Objekts mit zusätzlichen Eigenschaften; das ist etwas anderes als die Prüfung eines frischen Objektliterals. TypeScript: Object Types und Excess Property Checks
Eine öffentliche Antwort wird stattdessen tatsächlich konstruiert:
function toPublicProfile(user: User): PublicProfile {
return {
id: user.id,
name: user.name,
};
}
const response = toPublicProfile(storedUser);
console.log(JSON.stringify(response));
// {"id":7,"name":"Mina"}
Hier legt der Rückgabetyp den Vertrag fest, und die Funktion erzeugt das passende Objekt. Beide Teile sind nötig. Ebenso wenig ersetzt ein TypeScript-Typ die Prüfung eingehender JSON-Daten oder die serverseitige Berechtigungsprüfung. Typinformationen werden beim Übersetzen entfernt. TypeScript: Erased Types
Record: vollständige Zuordnungen für bekannte Schlüssel
Eine Buchungsoberfläche braucht für jeden Status eine Beschriftung:
type BookingStatus = "pending" | "confirmed" | "cancelled";
const statusLabels: Record<BookingStatus, string> = {
pending: "Ausstehend",
confirmed: "Bestätigt",
cancelled: "Storniert",
};
function labelFor(status: BookingStatus): string {
return statusLabels[status];
}
// @ts-expect-error: Die Beschriftung für cancelled fehlt
const incompleteLabels: Record<BookingStatus, string> = {
pending: "Ausstehend",
confirmed: "Bestätigt",
};
Eine neue Statusvariante verlangt damit auch eine Ergänzung der Beschriftungen. Das ist besonders nützlich, wenn Zustände aus einem fachlichen Modul in eine Oberfläche übersetzt werden – wie im Beispiel zur modularen Webarchitektur.
Record mit string garantiert keinen vorhandenen Eintrag
Eine endliche Union wie BookingStatus ist etwas anderes als der allgemeine Typ string:
const translations: Record<string, string> = {};
console.log(translations["missing"]); // undefined
Das Objekt muss nicht jeden denkbaren Text als Schlüssel besitzen. Die Compiler-Option noUncheckedIndexedAccess berücksichtigt beim Zugriff auf solche nicht ausdrücklich bekannten Schlüssel zusätzlich undefined. Bei dynamischen Wörterbüchern hilft das, fehlende Einträge sichtbar zu machen. Compiler-Option noUncheckedIndexedAccess
Required und Readonly: zwei weitere Grenzen
Required<T> verlangt vorhandene Eigenschaften. Es füllt fehlende Werte nicht selbst aus:
type Preferences = {
theme?: "light" | "dark";
locale?: string;
};
const defaults: Required<Preferences> = {
theme: "dark",
locale: "de",
};
// @ts-expect-error: locale ist durch Required verpflichtend
const incompleteDefaults: Required<Preferences> = { theme: "dark" };
Readonly<T> verhindert Zuweisungen an die betrachteten Eigenschaften im Typmodell. Es friert das JavaScript-Objekt nicht ein und wirkt nicht automatisch auf verschachtelte Objekte:
const readonlyUser: Readonly<User> = {
...storedUser,
profile: { ...storedUser.profile },
};
// @ts-expect-error: name ist auf dieser Ebene schreibgeschützt
readonlyUser.name = "Alex";
readonlyUser.profile.city = "Bochum"; // weiterhin typkorrekt
Die letzte Zeile verändert eine Eigenschaft des inneren Objekts. Die Referenz readonlyUser.profile selbst wird dabei nicht ersetzt. Die Unterscheidung zwischen einer schreibgeschützten Eigenschaft und ihrem veränderbaren Inhalt beschreibt das Handbuch zu readonly Properties.
Häufige Fragen zu TypeScript Utility Types
Wann nehme ich Pick, wann Omit?
Pick eignet sich, wenn die erlaubten Felder ausdrücklich feststehen sollen. Omit eignet sich, wenn eine interne Variante dem Ausgangstyp weitgehend folgen und nur bestimmte Felder ausschließen soll. Entscheidend ist das gewünschte Verhalten bei späteren Ergänzungen.
Kann ich Partial und Pick kombinieren?
Ja. Partial<Pick<User, "name" | "email">> erlaubt optionale Änderungen genau dieser beiden Felder. Das ist für ein Profilformular meist präziser als Partial<User>.
Macht Readonly ein Objekt unveränderbar?
Es begrenzt den typgeprüften Schreibzugriff. Eine Laufzeitgarantie oder einen rekursiven Schutz erzeugt es nicht. Auch andere Referenzen auf dasselbe Objekt müssen bei einem Unveränderlichkeitskonzept berücksichtigt werden.
Prüfen Utility Types die Antwort einer API?
Nein. Die tatsächlichen Daten müssen zur Laufzeit geprüft werden. Ein Type-Cast macht eine fremde Antwort nicht zuverlässig und entfernt keine unerwarteten Felder.
Für die nächste Schnittstelle würde ich zuerst die erlaubten Eingaben und Ausgaben aufschreiben. Danach lässt sich entscheiden, ob ein Utility Type diesen Vertrag sinnvoll aus einem vorhandenen Modell ableitet – oder ob ein eigener, kleiner Typ verständlicher ist.