Ein Mapped Type baut einen neuen Objekttyp, indem er über eine Menge von Schlüsseln iteriert. { [K in keyof T]: boolean } heißt "für jeden Schlüssel K von T eine Eigenschaft namens K vom Typ boolean":
Füge Features einen Schlüssel hinzu, und Flags<Features> verlangt ihn ebenfalls. Mapped Types existieren nur beim Kompilieren: Sie beschreiben Objekte, sie erzeugen keine.
Die Syntax
Ein Mapped Type hat drei Teile: eine Schlüsselvariable, eine Union von Schlüsseln, über die iteriert wird, und den Eigenschaftstyp, der die Schlüsselvariable verwenden darf.
type MappedType = {
[K in Keys]: PropertyType; // K takes each member of Keys in turn
};
Keys kann jede Union aus Strings, Zahlen oder Symbolen sein. Sie muss nicht aus keyof stammen:
Genau das macht Record<Size, number>; Record ist ein einzeiliger Mapped Type. Der Eigenschaftstyp kann den Schlüssel verwenden: { [K in keyof T]: T[K] } kopiert jeden Eigenschaftstyp über einen Indexed Access, und { [K in keyof T]: T[K] | null } macht jede Eigenschaft nullable.
Modifikatoren: readonly und ? mit + und -
Ein Mapped Type kann die Modifikatoren readonly und optional (?) bei allen Eigenschaften auf einmal hinzufügen oder entfernen. Ein vorangestelltes + fügt hinzu (das ist der Standard, wenn du nichts schreibst), ein - entfernt:
Die letzte Zeile zeigt, dass readonly nur eine Regel beim Kompilieren ist: Die Zuweisung wurde gemeldet (und hier mit @ts-expect-error unterdrückt), aber das erzeugte JavaScript hat sie trotzdem ausgeführt. -? macht mehr, als das Fragezeichen zu entfernen: Es entfernt auch undefined aus dem Eigenschaftstyp, also lehnt AllRequired<Account> das Objekt { id: 1, email: undefined } mit error TS2322: Type 'undefined' is not assignable to type 'string' ab.
Wie Partial, Readonly, Pick und Record aufgebaut sind
Die eingebauten Utility Types, die Objekte umformen, sind Mapped Types. Das sind die Definitionen aus TypeScripts eigener lib.es5.d.ts:
type Partial<T> = { [P in keyof T]?: T[P] };
type Required<T> = { [P in keyof T]-?: T[P] };
type Readonly<T> = { readonly [P in keyof T]: T[P] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Record<K extends keyof any, T> = { [P in K]: T };
Sie zu lesen, ist eine gute Übung: Pick iteriert nur über die Schlüssel, die du übergibst, und Record ignoriert jeden Quelltyp und gibt jedem Schlüssel denselben Werttyp. Omit ist kein eigener Mapped Type: Es ist Pick<T, Exclude<keyof T, K>>. Die Referenz der Utility Types listet alle auf.
Key Remapping mit as
Eine as-Klausel nach dem Schlüssel ändert den Eigenschaftsnamen. Zusammen mit Template Literal Types lassen sich so aus alten Namen neue erzeugen:
string & K steht da, weil keyof T auch Schlüssel vom Typ number und symbol enthalten kann und Capitalize nur Strings akzeptiert. Die Intersection behält die String-Schlüssel und verwirft den Rest.
Schlüssel mit never filtern
Ergibt die as-Klausel für einen Schlüssel never, wird dieser Schlüssel entfernt. Mit einem Conditional Type kannst du Eigenschaften je nach ihrem Typ behalten oder verwerfen:
Typen wie DataOnly sind nützlich, um zu beschreiben, was JSON.stringify übersteht oder was ein Formular bearbeitet: die Felder ohne die Methoden.
Welche Modifikatoren erhalten bleiben
Ein Mapped Type über keyof T für einen Typ T heißt homomorph, und er übernimmt die Modifikatoren readonly und ? jeder Eigenschaft von T. Ein Mapped Type über eine einfache Union von Schlüsseln beginnt ohne Modifikatoren:
type Account = { readonly id: number; email?: string };
type Copy<T> = { [K in keyof T]: T[K] };
type A = Copy<Account>;
// { readonly id: number; email?: string } (modifiers kept)
type B = { [K in "id" | "email"]: Account[K] };
// { id: number; email: string | undefined } (email is required now)
B hat im Typ von email immer noch undefined, weil Account["email"] es enthält, aber die Eigenschaft selbst ist nicht mehr optional: { id: 1 } wird abgelehnt. Deshalb iterieren Partial<T> und Co. über keyof T: So behalten sie alles, was sie nicht ausdrücklich ändern.
Ein generischer homomorpher Mapped Type, angewendet auf ein Array oder Tupel, erzeugt ein Array oder Tupel, kein Objekt mit numerischen Schlüsseln. Readonly<string[]> ist readonly string[], und mit type Box<T> = { [K in keyof T]: { value: T[K] } } ist Box<[string, number]> gleich [{ value: string }, { value: number }].
Mapped Types und Laufzeitcode
Weil ein Mapped Type ein Objekt nur beschreibt, braucht eine Funktion, die ein solches Objekt baut, ihre eigene Schleife zur Laufzeit. Der Typ beschreibt dann ihr Ergebnis:
Die Callbacks bekommen ihre Parametertypen vom Mapped Type (v ist im einen string, im anderen number), ohne jede Annotation. Falls du hier nach der Klasse Map statt nach Typ-Mapping gesucht hast, lies Map in TypeScript.
Häufig gestellte Fragen
Was ist ein Mapped Type in TypeScript?
Ein Typ, der einen Objekttyp erzeugt, indem er über eine Union von Schlüsseln iteriert: { [K in Keys]: SomeType }. Meist sind die Schlüssel keyof T, sodass der neue Typ dieselben Schlüssel wie T mit umgewandelten Eigenschaftstypen hat, wie in type Flags<T> = { [K in keyof T]: boolean }.
Was bedeuten +readonly, -readonly, +? und -? in einem Mapped Type?
Sie fügen Modifikatoren hinzu oder entfernen sie. readonly oder +readonly macht jede Eigenschaft readonly, -readonly entfernt readonly. ? oder +? macht jede Eigenschaft optional, -? macht sie verpflichtend und entfernt außerdem undefined aus ihren Typen. Required<T> ist mit -? geschrieben.
Wie benenne ich Schlüssel in einem Mapped Type um?
Mit einer as-Klausel nach dem Schlüssel: { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] } macht aus name den Schlüssel getName. Wird ein Schlüssel in der as-Klausel auf never abgebildet, verschwindet er, und so filterst du Eigenschaften.
Ist ein Mapped Type dasselbe wie Map in TypeScript?
Nein. Ein Mapped Type ist eine Typtransformation beim Kompilieren und erzeugt keinen Code. Map<K, V> ist die JavaScript-Klasse Map, eine Sammlung von Schlüssel-Wert-Paaren zur Laufzeit. Wer nach "typescript map type" sucht, meint oft das eine oder das andere.
Wie sind Partial und Readonly implementiert?
Als Mapped Types in der Standardbibliothek: type Partial<T> = { [P in keyof T]?: T[P] } und type Readonly<T> = { readonly [P in keyof T]: T[P] }. Pick und Record sind ebenfalls Mapped Types; Omit ist Pick kombiniert mit Exclude.