Record<K, V> ist ein eingebauter Utility Type für ein Objekt, dessen Schlüssel den Typ K und dessen Werte alle den Typ V haben. Mit string-Schlüsseln beschreibt er ein Wörterbuch; mit einer Union aus Literal-Schlüsseln beschreibt er ein Objekt, das genau diese Schlüssel haben muss.
Record existiert nur im Typsystem. Zur Laufzeit sind beide Objekte einfache JavaScript-Objekte, sie funktionieren also mit Objektliteralen, Spread, JSON.stringify und allem anderen, was ein Objekt nimmt.
Syntax und Definition
Record<Keys, Value>
Keys muss etwas sein, das ein Objektschlüssel sein kann: string, number, symbol, eine Union aus String- oder Zahl-Literalen oder ein Template Literal Type. Value kann jeder Typ sein. Die gesamte Definition in der Standardbibliothek von TypeScript ist eine Zeile, ein Mapped Type:
type Record<K extends keyof any, T> = {
[P in K]: T;
};
keyof any ist string | number | symbol, die Menge aller möglichen Schlüsseltypen. [P in K]: T erzeugt für jedes Mitglied von K eine Eigenschaft vom Typ T. Das erklärt beide Verhaltensweisen unten: Ein breites K wie string ergibt eine Index-Signatur (jeder Schlüssel), und ein Union-K ergibt pro Mitglied eine verpflichtende Eigenschaft.
Union-Schlüssel: Jeder Schlüssel ist verpflichtend
Sind die Schlüssel eine Union aus Literalen, muss ein Record jeden davon auflisten und keinen anderen. Damit wird der Compiler zur Checkliste:
Füge Status den Wert "cancelled" hinzu, und beide Objekte kompilieren nicht mehr, bis du dem neuen Status ein Label und eine Farbe gibst. Einen Schlüssel wegzulassen oder einen hinzuzufügen, der nicht in der Union steht, ist ein Compilerfehler:
index.ts(4,7): error TS2741: Property 'error' is missing in type '{ idle: string; loading: string; success: string; }' but required in type 'Record<Status, string>'.
index.ts(11,54): error TS2353: Object literal may only specify known properties, and 'paused' does not exist in type 'Record<Status, string>'.
Dasselbe funktioniert mit einem String-Enum als Schlüsseltyp: Record<Color, string> verlangt einen Eintrag pro Enum-Member.
Record<string, T> und der fehlende Schlüssel
Mit string-Schlüsseln ist jeder Schlüssel erlaubt, und TypeScript typisiert jeden Zugriff als T, auch für einen Schlüssel, den es nicht gibt. Zur Laufzeit ergibt ein fehlender Schlüssel undefined:
Das ist der häufigste Fehler mit Record. Drei Wege, damit umzugehen: vor dem Lesen mit in oder Object.hasOwn prüfen, den Wert als V | undefined deklarieren oder die Compiler-Option noUncheckedIndexedAccess einschalten, die im ganzen Projekt jedem Lesezugriff über eine Index-Signatur ein | undefined hinzufügt. Ein Record mit Union-Schlüsseln hat dieses Problem nicht, weil jeder Schlüssel garantiert existiert.
Partial<Record<K, V>>: nur einige Schlüssel
Um eine Union von Schlüsseln zu verwenden, ohne alle zu verlangen, umschließe den Record mit Partial. Lesezugriffe geben dann V | undefined zurück, und das ist ehrlich:
Ein falsch geschriebener Schlüssel wie jp ist trotzdem ein Fehler, und das ist der Vorteil gegenüber Record<string, string>.
Über einen Record iterieren
Object.keys, Object.values und Object.entries funktionieren alle. Der Haken ist der Schlüsseltyp: Object.keys gibt string[] zurück und Object.entries gibt [string, V][] zurück, nie deine Union von Schlüsseln:
TypeScript belässt die Schlüssel absichtlich bei string: Ein Objekt kann zur Laufzeit mehr Eigenschaften haben, als sein Typ auflistet, also wäre das Versprechen Plan[] im Allgemeinen unsicher. Für ein Objekt, das du aus einem Literal erzeugt hast, wie seats, ist der Cast sicher.
Einen Record aus Daten bauen
Records sind das übliche Ergebnis, wenn du ein Array gruppierst oder indizierst. Beginne mit einem leeren Objekt vom Record-Typ und fülle es:
Book["genre"] verwendet die Union aus dem Interface als Schlüsseltyp wieder, also verlangt byGenre einen neuen Eintrag, sobald du Book ein Genre hinzufügst.
Record<string, unknown> und Interfaces
Record<string, unknown> ist ein gängiger Typ für "irgendein Objekt mit String-Schlüsseln". Er akzeptiert Objektliterale und Werte, die mit einem type-Alias typisiert sind, aber ein Interface wird abgelehnt:
index.ts(11,11): error TS2345: Argument of type 'User' is not assignable to parameter of type 'Record<string, unknown>'.
Index signature for type 'string' is missing in type 'User'.
Interfaces lassen sich per Declaration Merging erweitern, also nimmt TypeScript nicht an, dass sie zu einer Index-Signatur passen; Type Aliases lassen sich nicht wieder öffnen, also würde ein type User = { name: string } durchgehen. Die üblichen Lösungen: stattdessen object akzeptieren (du kannst trotzdem Object.keys darauf aufrufen), die Funktion generisch machen (<T extends object>(obj: T)) oder User mit type deklarieren (die Seite interface vs type behandelt die übrigen Unterschiede).
Record vs Index-Signatur vs Map
Record<K, V> | { [key: string]: V } | Map<K, V> | |
|---|---|---|---|
| Existiert zur Laufzeit | nein, ein einfaches Objekt | nein, ein einfaches Objekt | ja, eine Klasse |
| Feste Menge von Schlüsseln | ja, mit einer Union als K | nein | nein |
| Schlüsseltypen | string, number, symbol, Literal-Unions, Template-Muster | string, number, symbol, Template-Muster | alles, auch Objekte |
| Typ beim Zugriff auf fehlenden Schlüssel | V (mit string-Schlüsseln) | V | V | undefined von get |
| Mit benannten Eigenschaften mischen | über eine Intersection & | ja, im selben Typ | nein |
| JSON und Spread | ja | ja | nein, erst umwandeln |
| Größe | Object.keys(r).length | Object.keys(o).length | m.size |
| Häufiges Hinzufügen und Löschen | funktioniert | funktioniert | dafür gebaut |
Nimm Record mit Union-Schlüsseln, wann immer die Menge der Schlüssel bekannt ist: Nur so wird geprüft, dass jeder Schlüssel vorhanden ist. Für offene String-Schlüssel sind Record<string, V> und eine Index-Signatur austauschbar, und viele Codebasen bevorzugen Record wegen der Lesbarkeit. Greif zu einer Map, wenn Schlüssel zur Laufzeit hinzukommen und wegfallen, wenn Schlüssel keine Strings sind oder wenn du Größe und Einfügereihenfolge ohne Zusatzaufwand brauchst.
Häufig gestellte Fragen
Was ist Record in TypeScript?
Record<K, V> ist ein eingebauter Utility Type für ein Objekt, dessen Schlüssel vom Typ K und dessen Werte alle vom Typ V sind. Record<string, number> ist ein Objekt mit beliebigen String-Schlüsseln und Zahlenwerten; Record<"en" | "de", string> ist ein Objekt mit genau den Schlüsseln en und de, beide mit Strings.
Was ist der Unterschied zwischen Record und Map in TypeScript?
Record ist ein Typ für ein einfaches JavaScript-Objekt, funktioniert also mit Objektliteralen, JSON und Spread und verschwindet beim Kompilieren. Map ist eine Klasse zur Laufzeit mit get, set, has und size, behält die Einfügereihenfolge für alle Schlüssel, akzeptiert jeden Schlüsseltyp (auch Objekte), und get gibt V | undefined zurück. Nimm einen Record für feste oder JSON-artige Daten und eine Map für Schlüssel, die zur Laufzeit hinzukommen und wegfallen.
Was ist der Unterschied zwischen Record<string, T> und { [key: string]: T }?
Für Werte ist es derselbe Typ: Record<string, T> wird zu einem Objekttyp mit String-Index-Signatur aufgelöst. Zwei kleine Unterschiede: Eine Index-Signatur kann einen Namen tragen und im selben Typ neben anderen Eigenschaften stehen, und keyof Record<string, T> ist string, während keyof { [key: string]: T } gleich string | number ist.
Wie iteriere ich in TypeScript über einen Record?
Nimm Object.entries(record) für Schlüssel-Wert-Paare, Object.keys für Schlüssel und Object.values für Werte. Die Schlüssel kommen als string zurück, nicht als K, weil ein Objekt zur Laufzeit zusätzliche Schlüssel haben kann. Hat der Record eine Union bekannter Schlüssel, caste: (Object.keys(r) as Array<keyof typeof r>).
Wie mache ich nur einige Schlüssel eines Records verpflichtend?
Mit Union-Schlüsseln verlangt Record<K, V> jeden Schlüssel. Umschließe ihn mit Partial, um alle optional zu machen: Partial<Record<Lang, string>>. Für eine Mischung bildest du eine Intersection: Record<"en", string> & Partial<Record<"de" | "fr", string>> verlangt en und erlaubt die anderen.