Mapped Types: Bestehende Typen systematisch transformieren
<T>
type
TypeScript · Mapped Types · Type-Level Programming
Mapped Types: Bestehende Typen systematisch transformieren
Modifikatoren, Minus-Präfix und Key Remapping mit as

Wer für jede Variante eines TypeScript Interfaces manuell ein neues Interface pflegt, erzeugt über kurz oder lang Typdrift zwischen Original und Kopie. Mapped Types transformieren bestehende Typen systematisch mit der Syntax K in keyof T, setzen und entfernen readonly und optionale Modifikatoren gezielt und benennen Schlüssel mit as um, ganz ohne Duplizierung.

14 Min. Lesezeit keyof · readonly · optional · as-Remapping TypeScript 5.x

1. Was Mapped Types wirklich lösen

Ein Mapped Type transformiert einen bestehenden Typ systematisch, indem er über dessen Property-Keys iteriert und für jeden Key eine neue Eigenschaft ableitet. Die Alternative dazu ist die manuelle Pflege von zwei oder mehr Interfaces, die dieselbe Feldliste beschreiben, nur mit unterschiedlichen Modifikatoren wie readonly oder optional. Sobald jemand ein Feld im Ausgangstyp hinzufügt, umbenennt oder entfernt, muss diese Änderung von Hand in jeder Kopie nachgezogen werden, was in der Praxis fast immer vergessen wird.

Genau dieses Synchronisationsproblem lösen Mapped Types strukturell: Statt eine Kopie zu pflegen, wird der neue Typ als Funktion des Ausgangstyps definiert. Ändert sich Product, ändert sich automatisch auch Partial<Product>, Readonly<Product> oder jeder selbst definierte abgeleitete Typ mit. Diese Eigenschaft macht Mapped Types zum Fundament fast aller eingebauten Utility Types in TypeScript und zu einem der wichtigsten Werkzeuge für typsichere Bibliotheken, API-Clients und Formular-Validierung.

2. Grundsyntax: { [K in keyof T]: ... }

Die Grundsyntax eines Mapped Types lautet { [K in keyof T]: T[K] } und liest sich fast wie eine for-in-Schleife auf Typebene. keyof T erzeugt eine Union aus allen Property-Keys von T, K in ... iteriert über jedes Element dieser Union, und T[K] liest den Typ der jeweiligen Eigenschaft aus. Das Ergebnis ist ein neuer Objekttyp mit derselben Struktur wie T, dessen einzelne Felder aber frei transformiert werden können, bevor sie im neuen Typ landen.

Der Schlüsselbereich muss nicht zwingend aus keyof T stammen. Jede Union aus String-, Number- oder Symbol-Literalen funktioniert als Quelle, was Mapped Types auch für generische Hilfstypen wie ein Flags<Keys>-Muster nutzbar macht, das aus einer Liste von Feature-Namen ein Objekt mit booleschen Werten für jeden Namen erzeugt. Diese Flexibilität unterscheidet Mapped Types von reinen Utility Types: Sie sind ein allgemeines Sprachfeature, keine Sammlung fertiger Spezialfälle.


// Basic mapped type: iterate over every key of T
type Stringify<T> = {
  [K in keyof T]: string;
};

interface Product {
  id: number;
  name: string;
  inStock: boolean;
}

// Every property becomes a string, keys and structure stay the same
type StringifiedProduct = Stringify<Product>;
// { id: string; name: string; inStock: string }

// Mapped types work with any key source, not just keyof
type Flags<Keys extends string> = {
  [K in Keys]: boolean;
};

type FeatureFlags = Flags<"darkMode" | "betaCheckout" | "newSearch">;
// { darkMode: boolean; betaCheckout: boolean; newSearch: boolean }

3. Modifikatoren: readonly und optional gezielt setzen

Innerhalb der eckigen Klammern eines Mapped Types lassen sich zwei Modifikatoren voranstellen: readonly macht die erzeugte Eigenschaft unveränderlich, und das Fragezeichen nach dem Key macht sie optional. Beide Modifikatoren lassen sich unabhängig voneinander setzen, kombinieren oder ganz weglassen. { readonly [K in keyof T]: T[K] } erzeugt aus jedem Feld ein schreibgeschütztes Pendant, { [K in keyof T]?: T[K] } macht jedes Feld optional, ohne die zugrunde liegenden Typen selbst zu verändern.

Wichtig ist der Unterschied zwischen einem Modifikator im Mapped Type und derselben Annotation direkt im Ausgangstyp: Der Mapped Type überschreibt den Modifikator nicht Feld für Feld von Hand, sondern wendet ihn strukturell auf alle Keys gleichzeitig an. Das spart nicht nur Tipparbeit, sondern garantiert auch, dass kein Feld beim manuellen Abschreiben vergessen wird, wenn der Ausgangstyp um weitere Properties wächst.

4. Modifikatoren entfernen: das Minus-Präfix -readonly und -?

Modifikatoren lassen sich nicht nur hinzufügen, sondern mit einem vorangestellten Minus auch gezielt entfernen. -readonly vor dem Key streicht einen zuvor gesetzten readonly-Modifikator wieder, und -? entfernt das Optional-Flag und macht ein Feld wieder verpflichtend. Das ist besonders nützlich, wenn ein bereits transformierter Typ als Ausgangspunkt für eine weitere Transformation dient, etwa wenn ein Draft-Typ mit optionalen Feldern am Ende der Bearbeitung wieder vollständig ausgefüllt sein muss, bevor er gespeichert wird.

Ohne das Minus-Präfix müsste man für diesen Fall einen komplett neuen Typ von Hand schreiben, der zufällig dieselbe Struktur wie das Original hat. Mit -readonly und -? bleibt die Ableitung transparent nachvollziehbar: Wer den Code liest, sieht sofort, dass CompleteOrder exakt Order entspricht, nur über den Umweg über einen optionalen Zwischenschritt. Das + vor einem Modifikator ist übrigens der Standardfall und wird fast nie explizit ausgeschrieben, weil TypeScript es implizit annimmt.


// Adding modifiers: readonly and optional
type Frozen<T> = {
  readonly [K in keyof T]: T[K];
};

type Draft<T> = {
  [K in keyof T]?: T[K];
};

interface Order {
  id: number;
  total: number;
  items: string[];
}

type FrozenOrder = Frozen<Order>;
// { readonly id: number; readonly total: number; readonly items: string[] }

// Removing modifiers with the minus prefix
type Unfrozen<T> = {
  -readonly [K in keyof T]: T[K];
};

type Complete<T> = {
  [K in keyof T]-?: T[K];
};

type MutableOrder = Unfrozen<FrozenOrder>;
// readonly is stripped again, all properties become writable

type PartialOrder = Draft<Order>;
type CompleteOrder = Complete<PartialOrder>;
// -? removes the optional modifier added by Draft, all fields required again

5. Partial<T> von Grund auf nachbauen

Der eingebaute Utility Type Partial<T> aus lib.es5.d.ts ist selbst nichts anderes als ein Mapped Type: { [K in keyof T]?: T[K] }. Wer diesen einen Satz Code selbst tippt, versteht sofort, warum Partial<T> keine Magie ist, sondern eine von vielen möglichen Ableitungen der Grundsyntax. Diese Erkenntnis ist der eigentliche Lerneffekt: Sobald die Mapped-Type-Syntax sitzt, lassen sich nicht nur die eingebauten Utility Types nachvollziehen, sondern auch beliebige eigene Varianten für projektspezifische Anforderungen bauen.

In der Praxis eignet sich ein selbstgebautes MyPartial<T> hervorragend für PATCH-Endpunkte oder Update-Funktionen, bei denen der Aufrufer nur die tatsächlich geänderten Felder mitschicken soll. Der Compiler erzwingt dabei trotzdem, dass keine unbekannten Feldnamen oder falschen Werttypen durchrutschen, weil die Struktur weiterhin von Customer abgeleitet ist. Ändert sich Customer um ein neues Feld, ist MyPartial<Customer> automatisch auf dem aktuellen Stand, ohne dass jemand den Patch-Typ manuell nachpflegen muss.


// Reimplementing the built-in Partial<T> utility type
type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

interface Customer {
  id: number;
  email: string;
  newsletter: boolean;
}

// Every property becomes optional, same shape otherwise
type CustomerPatch = MyPartial<Customer>;

function updateCustomer(id: number, patch: MyPartial<Customer>): void {
  // patch.email, patch.newsletter, patch.id are all optional here
  // the caller only needs to send the fields that actually changed
}

updateCustomer(42, { newsletter: false });
updateCustomer(42, { email: "new@example.com", newsletter: true });

// This is exactly what lib.es5.d.ts ships as the built-in Partial<T>

6. Readonly<T> und Required<T> selbst implementieren

Readonly<T> und Required<T> folgen demselben Bauprinzip wie Partial<T>, nur mit anderen Modifikatoren. Readonly<T> setzt readonly vor jeden Key und macht damit ein Objekt nach der Initialisierung unveränderlich, was sich besonders für Konfigurationsobjekte oder Redux-artige State-Strukturen eignet, die nicht versehentlich mutiert werden sollen. Required<T> nutzt stattdessen -? und entfernt alle Optional-Flags, wodurch aus einem Typ mit teilweise optionalen Feldern ein Typ wird, in dem jedes Feld zwingend vorhanden sein muss.

Beide Varianten lassen sich beliebig kombinieren, etwa in Required<Readonly<Config>> für ein vollständig ausgefülltes, unveränderliches Konfigurationsobjekt nach dem Laden aus einer Umgebungsvariable. Wichtig ist, dass readonly nur zur Compile-Zeit greift: Es verhindert Zuweisungen im TypeScript-Compiler, hat aber keine Laufzeitwirkung. Wer echte Unveränderlichkeit zur Laufzeit braucht, muss zusätzlich Object.freeze() einsetzen, das Typsystem allein schützt nur vor versehentlichen Zuweisungen im eigenen Code.


// Reimplementing Readonly<T> and Required<T>
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

interface Config {
  apiUrl: string;
  timeoutMs?: number;
  retries?: number;
}

type FrozenConfig = MyReadonly<Config>;
const config: FrozenConfig = { apiUrl: "https://api.mironsoft.de", timeoutMs: 5000, retries: 3 };
// config.apiUrl = "other"; // Error: cannot assign to read-only property

type FullConfig = MyRequired<Config>;
// timeoutMs and retries lose their ? and become mandatory

function bootstrap(config: FullConfig): void {
  // no need for `config.timeoutMs ?? 5000` fallbacks anymore
}

7. Key Remapping mit as: Schlüssel umbenennen

Seit TypeScript 4.1 erlaubt die as-Klausel innerhalb eines Mapped Types, nicht nur den Werttyp, sondern auch den Schlüsselnamen selbst zu transformieren: { [K in keyof T as NeuerKey]: T[K] }. In Kombination mit Template Literal Types entstehen daraus mächtige Ableitungen wie ein Getters<T>-Typ, der aus jeder Property automatisch eine passende Getter-Methode mit vorangestelltem get und großgeschriebenem Feldnamen erzeugt, ganz ohne die einzelnen Methoden von Hand aufzuzählen.

Key Remapping ist der Baustein, der Mapped Types von einer reinen Modifikator-Anwendung zu einem vollwertigen Transformationswerkzeug macht. Capitalize<string & K> in der Template-Literal-Expression ist notwendig, weil K theoretisch auch ein Symbol oder eine Zahl sein kann, TypeScript aber nur mit String-Keys in Template Literal Types arbeitet. Das & string schränkt den Key auf den String-Fall ein, bevor Capitalize angewendet wird.


// Key remapping with `as`: turn properties into getter method names
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

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

type ProductGetters = Getters<Product>;
// { getId: () => number; getName: () => string; getPrice: () => number }

// Filtering keys: remap unwanted keys to `never` to drop them entirely
type OmitByType<T, TypeToOmit> = {
  [K in keyof T as T[K] extends TypeToOmit ? never : K]: T[K];
};

// Removes every property whose value type is a function
type OnlyDataFields = OmitByType<ProductGetters, (...args: never[]) => unknown>;
// {} - every property was a getter function, so all keys were filtered out

8. Schlüssel filtern: bedingtes Remapping mit as never

Wird ein Key im as-Ausdruck auf never gemappt, verschwindet die zugehörige Eigenschaft komplett aus dem Ergebnistyp. Dieses Muster macht bedingtes Filtern möglich: { [K in keyof T as T[K] extends Function ? never : K]: T[K] } behält nur die Felder, deren Werttyp keine Funktion ist, und entfernt alle anderen vollständig aus dem Typ, statt sie nur auf never zu setzen, was den Key selbst nicht entfernen würde.

Der Unterschied zwischen einem auf never gemappten Wert und einem auf never gemappten Key ist entscheidend: { [K in keyof T]: never } behält alle Keys, nur mit dem Werttyp never, während { [K in keyof T as never]: T[K] } den Typ komplett auf {} reduziert. Dieses Filterprinzip ist die Grundlage dafür, wie eingebaute Utility Types wie Pick<T, K> und Omit<T, K> unter der Haube funktionieren, auch wenn Omit selbst technisch über Exclude und Pick definiert ist.

9. Mapped Types im direkten Vergleich

Fast jede der bisher gezeigten Transformationen lässt sich auch ohne Mapped Types umsetzen, indem man den Zieltyp einfach von Hand als eigenständiges Interface aufschreibt. Der Unterschied zeigt sich erst, wenn der Ausgangstyp sich ändert: Mapped Types bleiben automatisch synchron, manuell duplizierte Typen driften auseinander, sobald jemand vergisst, die Kopie mitzupflegen. Die folgende Übersicht stellt die naive, manuelle Variante der jeweiligen Mapped-Type-Lösung gegenüber.

Aufgabe Naiv / manuell Mapped-Type-Pattern Vorteil
Alle Felder optional machen Jedes Feld manuell mit ? duplizieren Partial<T> Ein Ort für Änderungen, keine Drift
Alle Felder readonly machen Interface komplett neu abschreiben Readonly<T> Immutability ohne Codeverdopplung
Optionale Felder wieder verpflichten Separates Interface pflegen Required<T> mit -? Bleibt synchron mit Basistyp
Properties in Getter-Methoden umwandeln Für jede Property eine Methode von Hand schreiben Key Remapping mit as Skaliert automatisch mit neuen Feldern
Bestimmte Felder aus einem Typ entfernen Neues Interface ohne die Felder pflegen as never im Mapped Type Typ bleibt an Quelle gekoppelt

In größeren Codebasen mit vielen Domänentypen zahlt sich diese Entkopplung besonders aus: Ein zentrales Product-Interface, aus dem per Mapped Type alle Lese-, Schreib- und Patch-Varianten abgeleitet werden, lässt sich an einer einzigen Stelle erweitern. Manuell gepflegte Parallel-Interfaces erzeugen dagegen genau die Art von stillem Drift, die Codereviews selten zuverlässig auffangen, weil sich beide Typen zunächst korrekt kompilieren, auch wenn sie längst nicht mehr dasselbe beschreiben.

Mironsoft

TypeScript-Tooling, Typsicherheit und Build-Pipelines für Magento- und Hyvä-Projekte

Typsichere Build-Skripte und Frontend-Tooling gesucht?

Wir entwickeln TypeScript-Tooling für Magento- und Hyvä-Projekte, von typsicheren Build-Skripten über generierte API-Clients bis zu wartbaren Utility-Type-Bibliotheken, die mit eurem Code mitwachsen, statt bei jeder Änderung zu brechen.

Type-Level Reviews

Audit bestehender Typdefinitionen auf Redundanz, Drift und fehlende Mapped-Type-Patterns

Build-Tooling

Typsichere Skripte für Deployment, Codegenerierung und CI/CD-Pipelines

Schulung & Pairing

Praxisnahes Onboarding für Teams, die von JavaScript zu TypeScript wechseln

10. Zusammenfassung

Mapped Types transformieren bestehende Typen systematisch über die Syntax { [K in keyof T]: ... }, statt sie manuell zu duplizieren. readonly und ? setzen Modifikatoren gezielt, -readonly und -? entfernen sie wieder, und beide Varianten lassen sich frei kombinieren. Partial<T>, Readonly<T> und Required<T> sind keine Magie, sondern selbst nur Mapped Types mit wenigen Zeilen Implementierung, wie der eigene Nachbau in diesem Artikel zeigt.

Key Remapping mit as erweitert Mapped Types von einer reinen Modifikator-Anwendung zu einem vollwertigen Transformationswerkzeug: Schlüssel lassen sich umbenennen, mit Template Literal Types kombinieren oder durch Mapping auf never vollständig aus dem Ergebnistyp entfernen. Wer diese Bausteine beherrscht, versteht nicht nur die eingebauten Utility Types, sondern kann eigene, projektspezifische Typtransformationen bauen, die automatisch mit dem Ausgangstyp mitwachsen.

Mapped Types: Bestehende Typen systematisch transformieren - Das Wichtigste auf einen Blick

Grundsyntax

{ [K in keyof T]: T[K] } iteriert über alle Property-Keys eines Typs und leitet daraus einen neuen Objekttyp ab.

Modifikatoren

readonly und ? hinzufügen, -readonly und -? gezielt wieder entfernen.

Utility Types selbst gebaut

Partial, Readonly und Required sind nur wenige Zeilen Mapped-Type-Code, keine Magie.

Key Remapping

as benennt Schlüssel um oder entfernt sie via never vollständig aus dem Ergebnistyp.

11. FAQ: Mapped Types in TypeScript

1Was ist ein Mapped Type in TypeScript?
Ein Konstrukt, das über die Property-Keys eines bestehenden Typs iteriert und daraus systematisch einen neuen Typ ableitet, statt Interfaces manuell zu duplizieren.
2Wie unterscheidet sich ein Mapped Type von einem normalen Interface?
Ein Mapped Type wird aus einem anderen Typ abgeleitet und bleibt automatisch synchron. Ein manuell gepflegtes Interface muss bei jeder Änderung von Hand nachgezogen werden.
3Was bewirkt das Minus-Präfix bei -readonly und -??
Entfernt einen zuvor gesetzten Modifikator wieder. -readonly macht ein Feld wieder beschreibbar, -? macht es wieder verpflichtend.
4Wie ist Partial<T> tatsächlich implementiert?
In lib.es5.d.ts als { [K in keyof T]?: T[K] } definiert, ein einfacher Mapped Type ohne versteckte Sonderlogik.
5Kann ich mit Mapped Types auch Schlüssel umbenennen?
Ja, seit TypeScript 4.1 über die as-Klausel, kombinierbar mit Template Literal Types für automatisch abgeleitete Namen.
6Wie entferne ich Eigenschaften aus einem Typ?
Über bedingtes Remapping auf never im as-Ausdruck. Ein auf never gemappter Key verschwindet vollständig aus dem Ergebnistyp.
7Homomorphe vs. nicht-homomorphe Mapped Types?
Homomorphe Mapped Types iterieren über keyof T und übernehmen bestehende Modifikatoren. Über eine explizite Union iterierende Mapped Types tun das nicht.
8Funktionieren Mapped Types mit Union-Typen als Schlüsselquelle?
Ja, jede Union aus String-, Number- oder Symbol-Literalen funktioniert als Schlüsselquelle, nicht nur keyof T.
9Wann eigenen Mapped Type statt eingebauter Utility verwenden?
Wenn die eingebauten Utility Types die Transformation nicht abbilden, etwa bei Key Remapping oder bedingter Filterung nach Werttyp.
10Performance-Unterschiede gegenüber manuellen Typen?
Zur Laufzeit keine, Typen werden beim Kompilieren entfernt. Bei tief verschachtelten Mapped Types kann sich die Typprüfzeit im Build spürbar erhöhen.