Mapped Types in TypeScript: Typen aus anderen Typen erzeugen

Mapped Types sind das Werkzeug, mit dem TypeScript aus einem bestehenden Typ automatisch neue erzeugt – und die Grundlage für Partial, Pick und Co. So baust du sie selbst.

Teilen

Du kennst wahrscheinlich schon Partial<T> oder Readonly<T> und nutzt sie, ohne groß darüber nachzudenken. Aber hast du dich mal gefragt, wie diese Typen eigentlich funktionieren? Die Antwort heißt Mapped Types – ein Mechanismus, mit dem du über die Eigenschaften eines Typs iterierst und daraus einen neuen Typ baust. In diesem Beitrag schauen wir uns an, wie Mapped Types aufgebaut sind, wie du Modifier gezielt setzt und entfernst und wie du am Ende eigene, wiederverwendbare Utility Types schreibst. Wenn du Generics und die eingebauten Utility Types schon kennst, ist das der nächste logische Schritt.

Was sind Mapped Types?

Ein Mapped Type nimmt einen bestehenden Typ und erzeugt daraus einen neuen, indem er über jede Eigenschaft läuft und für jede eine neue Regel definiert. Statt jede Property von Hand abzuschreiben, sagst du einmal: „Nimm alle Schlüssel und mach damit X.“ Das ist besonders praktisch, wenn sich der ursprüngliche Typ ändert – dein abgeleiteter Typ zieht dann automatisch mit.

Stell dir vor, du hast einen Typ für einen Nutzer und möchtest eine Variante, in der wirklich jedes Feld ein boolean ist – etwa um zu markieren, welche Felder im Formular schon ausgefüllt wurden:

type User = {
  id: number;
  name: string;
  email: string;
};

// Jede Eigenschaft von User wird zu boolean
type UserFlags = {
  [K in keyof User]: boolean;
};

// Ergebnis:
// {
//   id: boolean;
//   name: boolean;
//   email: boolean;
// }

Das [K in keyof User] ist der Kern: Es läuft über alle Schlüssel von User und weist jedem den Typ boolean zu.

Die Grundsyntax verstehen

Die Syntax besteht aus drei Teilen. Mit keyof T holst du dir alle Schlüssel eines Typs als Union. Mit K in ... iterierst du über diese Union. Und mit T[K] greifst du auf den Typ der jeweiligen Eigenschaft zu (das nennt man indexed access).

type User = {
  id: number;
  name: string;
};

// keyof User  ->  "id" | "name"
// User["name"] ->  string

// Ein Mapped Type, der den Originaltyp einfach kopiert:
type Copy<T> = {
  [K in keyof T]: T[K];
};

type UserCopy = Copy<User>;
// { id: number; name: string; }

Sobald du T[K] stehen lässt, bleibt der ursprüngliche Typ jeder Eigenschaft erhalten. Das ist die Basis, auf der fast alle nützlichen Varianten aufbauen – du veränderst dann nur noch die Modifier oder den Werttyp.

Modifier: readonly und optional hinzufügen und entfernen

Jede Eigenschaft kann zwei Modifier tragen: readonly und ? (optional). Mapped Types erlauben dir, diese gezielt zu setzen. Ein + fügt sie hinzu (das ist der Standard und kann weggelassen werden), ein - entfernt sie.

type User = {
  id: number;
  name?: string;
};

// Alle Felder optional machen -> so funktioniert Partial
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

// Alle Felder schreibgeschützt machen -> so funktioniert Readonly
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

// Optional wieder entfernen -> so funktioniert Required
type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

type StrictUser = MyRequired<User>;
// { id: number; name: string; }  -> name ist jetzt Pflicht

Damit hast du gerade drei der bekanntesten eingebauten Utility Types nachgebaut. Partial, Readonly und Required sind nichts anderes als kurze Mapped Types – der einzige Trick ist das -?, das die Optionalität wieder wegnimmt.

Key Remapping mit as

Seit TypeScript 4.1 kannst du mit dem Schlüsselwort as nicht nur die Werte, sondern auch die Schlüssel selbst umbenennen. Das öffnet die Tür für richtig mächtige Transformationen. Ein typisches Beispiel: aus jeder Eigenschaft automatisch einen Getter-Namen erzeugen.

type User = {
  name: string;
  age: number;
};

// Aus name wird getName, aus age wird getAge
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type UserGetters = Getters<User>;
// {
//   getName: () => string;
//   getAge: () => number;
// }

Über as kannst du Schlüssel auch komplett herausfiltern, indem du sie auf never abbildest. So entfernst du zum Beispiel alle Eigenschaften, deren Wert eine Funktion ist:

type Mixed = {
  id: number;
  name: string;
  save: () => void;
};

// Nur die Daten-Felder behalten, Methoden rausfiltern
type DataOnly<T> = {
  [K in keyof T as T[K] extends Function ? never : K]: T[K];
};

type MixedData = DataOnly<Mixed>;
// { id: number; name: string; }

Ein Schlüssel, der auf never gemappt wird, verschwindet einfach aus dem Ergebnistyp – ein eleganter Weg, um selektiv aufzuräumen.

Eigene Utility Types selbst bauen

Mit dem bisher Gelernten kannst du dir maßgeschneiderte Helfer schreiben, die dein Projekt lesbarer machen. Ein häufiger Wunsch: alle Felder eines Typs auf null erweitern, etwa für API-Antworten, in denen Felder fehlen können.

type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

type ApiUser = Nullable<{ id: number; name: string }>;
// { id: number | null; name: string | null; }

// Und rekursiv, damit auch verschachtelte Objekte erfasst werden:
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? DeepReadonly<T[K]>
    : T[K];
};

type Config = {
  server: { host: string; port: number };
};

const cfg: DeepReadonly<Config> = {
  server: { host: "localhost", port: 8080 },
};

// cfg.server.port = 3000; // Fehler: read-only bis in die Tiefe

Ein praktischer Tipp: Halte deine Mapped Types generisch (mit <T>), damit du sie auf beliebige Typen anwenden kannst. Und übertreib es nicht mit der Verschachtelung – wenn ein Mapped Type nach drei Zeilen niemand mehr versteht, ist ein einfacher, explizit geschriebener Typ oft die bessere Wahl.

Fazit

Mapped Types sind der Motor hinter den eingebauten Utility Types – und sobald du die Syntax [K in keyof T] verinnerlicht hast, entmystifizieren sich Partial, Readonly und Pick von selbst. Mit den Modifiern + und - steuerst du readonly und Optionalität präzise, und mit as benennst du Schlüssel um oder filterst sie heraus. Am meisten profitierst du davon, wenn du sie sparsam und generisch einsetzt: kleine, wiederverwendbare Helfer, die deinen Code trockener und sicherer machen. Probier es aus – bau dir als Übung einmal Pick und Omit selbst nach, dann sitzt das Konzept.