Utility Types in TypeScript: Partial, Pick, Omit, Record

1. Übersicht

Beim Schreiben von TypeScript wirst du auf Situationen stoßen, in denen du einen Type benötigst, der einem vorhandenen Type ähnelt, aber einige Felder fehlen oder sind überschüssig. Anstatt ein neues Interface zu deklarieren, stellt TypeScript eine Gruppe von Utility Types bereit, um vorhandene Types zu transformieren. Dieser Artikel behandelt die vier am häufigsten verwendeten Utility Types: Partial, Pick, Omit und Record, zusammen mit praktischen Beispielen und häufigen Fehlern. Nach dem Lesen wirst du wissen, wie du den richtigen Utility Type für jede Situation auswählst, anstatt Interfaces manuell neu zu schreiben.

2. Partial<T>

Partial<T> macht alle Felder von T optional. Nützlich beim Schreiben von Update-Funktionen, wo der Aufrufer nur einen Teil der Felder übergeben muss.

interface Customer {
  id: number;
  name: string;
  email: string;
  phone: string;
}

function updateCustomer(id: number, changes: Partial<Customer>): Customer {
  const current = findCustomerById(id); // angenommen, diese Funktion existiert bereits
  return { ...current, ...changes };
}

updateCustomer(1, { email: "new@mail.com" });

Partial<Customer> erstellt einen äquivalenten Type:

interface PartialCustomer {
  id?: number;
  name?: string;
  email?: string;
  phone?: string;
}

Dadurch akzeptiert updateCustomer ein Objekt mit nur email, ohne Fehler bei fehlenden Feldern zu melden.

Hinweis: Partial macht nur auf Ebene 1 optional (shallow). Wenn Customer ein verschachteltes Objekt-Feld hat, sind die inneren Felder immer noch erforderlich. Für tiefe Partialität musst du einen rekursiven Type selbst schreiben oder eine Bibliothek wie type-fest verwenden.

3. Pick<T, K>

Pick<T, K> wählt eine Teilmenge der Felder von T nach einer Liste von Keys K aus. Wird verwendet, wenn du nur einen Teil der Daten eines großen Types benötigst, z. B. einen DTO, der an den Client zurückgegeben wird.

interface Order {
  id: string;
  customerId: number;
  totalAmount: number;
  items: string[];
  createdAt: Date;
  internalNote: string;
}

type OrderSummary = Pick<Order, "id" | "totalAmount" | "createdAt">;

const summary: OrderSummary = {
  id: "ORD-001",
  totalAmount: 250_000,
  createdAt: new Date(),
};

OrderSummary hat nur noch 3 Felder. Wenn du zusätzlich die Felder items oder internalNote zu summary zuweist, wird der Compiler einen Fehler Object literal may only specify known properties melden.

3.1. Pick mit Union-Key

Der an Pick übergebene Key ist ein String-Literal-Union, daher kannst du ihn in eine separate Variable extrahieren, um sie wiederzuverwenden:

type OrderPublicFields = "id" | "totalAmount" | "createdAt";

type OrderSummary = Pick<Order, OrderPublicFields>;
type OrderListItem = Pick<Order, OrderPublicFields | "customerId">;

Diese Methode verhindert die Wiederholung der Key-Liste an mehreren Stellen, wenn du viele DTO-Varianten von derselben Entity hast.

4. Omit<T, K>

Omit<T, K> funktioniert umgekehrt wie Pick: Es entfernt die angegebenen Keys und behält den Rest bei. Wird üblicherweise verwendet, wenn du sensible Felder oder interne Felder verbergen möchtest.

interface User {
  id: number;
  username: string;
  passwordHash: string;
  role: string;
}

type PublicUser = Omit<User, "passwordHash">;

function toPublicUser(user: User): PublicUser {
  const { passwordHash, ...rest } = user;
  return rest;
}

Die Zeile const { passwordHash, ...rest } = user trennt passwordHash ab, rest enthält nur id, username und role — was genau zum Type PublicUser passt.

// Ausgabe von console.log(toPublicUser(user))
{ id: 1, username: "nonla_dev", role: "ADMIN" }

Hinweis: Omit wird auf Basis von Pick und Exclude definiert, funktioniert gut mit Object-Literal-Types, aber entfernt Überladungen oder diskriminierte Unions in komplexen Fällen nicht ordnungsgemäß. Bei Union-Types (type A = B | C) werden die Member von Omit<A, K> vor dem Entfernen zusammengeführt, was das Diskriminierungsmerkmal zerstören kann. Bei komplexen Unions solltest du erwägen, Types manuell zu schreiben, anstatt Omit direkt zu verwenden.

5. Record<K, T>

Record<K, T> erstellt einen Object-Type mit allen Keys aus K, wobei jeder Key einen Wert vom Typ T hat. Nützlich beim Modellieren von Maps/Dictionaries mit festgelegten Keys.

type PaymentStatus = "PENDING" | "PAID" | "CANCELLED";

interface StatusInfo {
  label: string;
  color: string;
}

const statusDisplay: Record<PaymentStatus, StatusInfo> = {
  PENDING: { label: "Zahlung ausstehend", color: "orange" },
  PAID: { label: "Bezahlt", color: "green" },
  CANCELLED: { label: "Storniert", color: "gray" },
};

function getStatusLabel(status: PaymentStatus): string {
  return statusDisplay[status].label;
}

Da statusDisplay als Record<PaymentStatus, StatusInfo> deklariert ist, erzwingt der Compiler, dass du alle drei Keys PENDING, PAID und CANCELLED haben musst. Wenn du einen Key vergisst, meldet TypeScript den Fehler bereits zur Compile-Zeit — das ist ein Vorteil gegenüber der üblichen Verwendung von { [key: string]: StatusInfo } (Index-Signaturen prüfen nicht auf vollständige Keys).

5.1. Record mit dynamischem Key

Wenn der Key kein String-Literal-Union ist, sondern einfach string, ist Record<string, T> äquivalent zur Index-Signatur:

const cache: Record<string, number> = {};
cache["order-1"] = 100;

In diesem Fall gibt es keine Überprüfung auf „vollständige Keys” mehr, da string unendlich viele Werte haben kann. Verwende Record<string, T> nur, wenn du wirklich ein freies Dictionary brauchst, ansonsten bevorzuge String-Literal-Unions, um die Exhaustive-Überprüfung des Compilers zu nutzen.

6. Kombination mehrerer Utility Types

Utility Types können verschachtelt werden, um komplexere Types zu erstellen, ohne neue Interfaces zu schreiben.

interface Product {
  id: string;
  name: string;
  price: number;
  stock: number;
  supplierNote: string;
}

type ProductUpdateRequest = Partial<Omit<Product, "id">>;

function updateProduct(id: string, data: ProductUpdateRequest) {
  // rufe repository.update(id, data) auf
}

updateProduct("P001", { price: 199_000 });

Omit<Product, "id"> entfernt das Feld id (keine Änderung des Primärschlüssels möglich), dann macht Partial die verbleibenden Felder optional. Ergebnis: Der Aufrufer kann nur die Felder übergeben, die er ändern möchte, und darf id nicht übergeben.

7. Tests zum Verifizieren des Verhaltens

Da Utility Types nur zur Compile-Zeit vorhanden sind, besteht die praktische Verifizierung darin, Unit-Tests für Funktionen zu schreiben, die diese Types verwenden, um sicherzustellen, dass das Runtime-Verhalten wie erwartet ist. Beispiel mit Vitest:

<!-- package.json dependency Äquivalent (nicht Maven) -->
import { describe, it, expect } from "vitest";

interface User {
  id: number;
  username: string;
  passwordHash: string;
  role: string;
}

type PublicUser = Omit<User, "passwordHash">;

function toPublicUser(user: User): PublicUser {
  const { passwordHash, ...rest } = user;
  return rest;
}

describe("toPublicUser", () => {
  it("entfernt passwordHash aus dem Ergebnis", () => {
    const user: User = {
      id: 1,
      username: "nonla_dev",
      passwordHash: "hash-123",
      role: "ADMIN",
    };

    const result = toPublicUser(user);

    expect(result).toEqual({
      id: 1,
      username: "nonla_dev",
      role: "ADMIN",
    });
    expect(result).not.toHaveProperty("passwordHash");
  });
});

Dieser Test bestätigt, dass die Funktion toPublicUser nicht nur in Bezug auf den Type korrekt ist, sondern auch zur Laufzeit — passwordHash verschwindet tatsächlich aus dem zurückgegebenen Objekt, es wird nicht nur auf der Type-Ebene „ausgeblendet”.

8. Häufige Fehler und wann Utility Types nicht verwendet werden sollten

  • Verwechslung: Pick/Omit löscht nicht die tatsächlichen Daten: Beide ändern nur den Type, sie beeinflussen nicht das Runtime-Objekt. Erst Destructuring (const { x, ...rest } = obj) entfernt tatsächlich ein Feld aus den Daten.
  • Missbrauch von Partial für vom API zurückgegebene Objekte: Wenn die Response tatsächlich immer alle Felder hat, zwingt dich Partial dazu, unnötige undefined-Prüfungen überall durchzuführen. Verwende Partial nur für Input (update/patch), nicht für Output, das garantiert vollständig ist.
  • Omit mit komplexen Union-Types: Wie in Abschnitt 4 erwähnt, kann Omit diskriminierte Unions zerstören. Wenn dein Type eine Union mehrerer Varianten ist (z. B. Shape = Circle | Square), schreibe den Type selbst oder verwende bedingte Mapped-Types, anstatt Omit direkt zu verwenden.
  • Record<string, T> statt klarem Interface: Wenn du alle Keys im Voraus kennst, deklariere einen String-Literal-Union und verwende Record<Key, T> für die Exhaustive-Überprüfung. Die willkürliche Verwendung von Record<string, T> verliert die Fähigkeit, fehlende/zusätzliche Keys zur Compile-Zeit zu erkennen.

Hinweis: Alle diese Utility Types funktionieren nur zur Compile-Zeit. Nach dem Build zu JavaScript verschwinden sie vollständig — es gibt keinen Runtime-Overhead, aber auch keine Runtime-Validierung. Wenn Daten von außen kommen (API, JSON-Datei), musst du sie immer noch mit Bibliotheken wie Zod oder io-ts validieren.

9. Fazit

Partial, Pick, Omit und Record helfen dir, Types wiederzuverwenden, anstatt doppelte Interfaces zu schreiben. Partial eignet sich für Update-artige Eingaben, Pick/Omit wird verwendet, um DTOs aus großen Entitäten zu erstellen, während Record ideal für Dictionaries mit festgelegten Keys ist und die Exhaustive-Überprüfung des Compilers nutzt. Ein klares Verständnis der Grenzen jedes Types — besonders das Shallow-Verhalten von Partial und die Union-Type-Probleme von Omit — wird dir helfen, später schwer zu debuggende Type-Fehler zu vermeiden.

typescriptutility-typestype-systemfrontend