7 Min. Lesezeit

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.