keyof nimmt einen Objekttyp und erzeugt die Union seiner Eigenschaftsnamen. keyof User ist unten der Typ "id" | "name" | "email", also kann eine Variable dieses Typs nur einen dieser drei Strings enthalten.
keyof existiert nur im Typsystem. Beim Kompilieren wird es entfernt, es prüft also zur Laufzeit nie etwas. Was es dir gibt, ist eine Garantie beim Kompilieren, dass ein Schlüssel eine echte Eigenschaft benennt: Benenne email im Interface um, und jedes "email", das als keyof User typisiert ist, wird zum Fehler.
keyof typeof: Schlüssel eines einfachen Objekts
keyof arbeitet mit Typen, nicht mit Werten. Für ein Objekt, das du als Wert geschrieben hast, holst du zuerst mit typeof seinen Typ und nimmst dann die Schlüssel:
Lies es von innen nach außen: typeof colors ist { red: string; green: string; blue: string }, und keyof davon ist die Union der Schlüssel. keyof colors ohne typeof scheitert mit error TS2749: 'colors' refers to a value, but is being used as a type here. Did you mean 'typeof colors'?.
So leitest du üblicherweise eine Union aus einer Nachschlagetabelle ab: Füge dem Objekt eine Farbe hinzu, und der Typ Color wächst mit. typeof hat auch eine zweite Bedeutung zur Laufzeit; die typeof-Seite behandelt beide.
Typisierter Eigenschaftszugriff mit Generics
Am häufigsten wird keyof als generischer Constraint verwendet. K extends keyof T heißt "K ist einer der Schlüssel von T", und T[K] ist der Typ der Eigenschaft unter diesem Schlüssel:
Der zweite Typparameter ist wichtig. Mit key: keyof T und ohne K ist der Rückgabetyp T[keyof T], die Union aller Eigenschaftstypen (hier string | number | boolean), und title.toUpperCase() würde mit error TS2339: Property 'toUpperCase' does not exist on type 'string | number | boolean' scheitern. Hältst du den genauen Schlüssel in K fest, bleibt der genaue Eigenschaftstyp erhalten. T[K] ist ein Indexed Access Type, beschrieben auf der Seite zu Indexed Access Types.
Indizieren mit einem einfachen string
Ein sehr häufiger Fehler tritt auf, wenn du ein Objekt mit einem string indizierst, der von woanders kommt:
Der Compiler gibt aus:
index.ts(10,10): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Settings'.
No index signature with a parameter of type 'string' was found on type 'Settings'.
Jeder beliebige String könnte bei read ankommen, auch "colour", und Settings sagt über diesen Schlüssel nichts. Zwei Lösungen: nur echte Schlüssel akzeptieren, oder einen unbekannten String prüfen, bevor du ihn verwendest.
name is keyof Settings ist ein Type Predicate: Wenn die Funktion true zurückgibt, behandelt TypeScript input innerhalb des if als Schlüssel. Die Seite zu Type Guards erklärt Predicates im Detail.
keyof mit Index-Signaturen: string | number
Bei einem Typ mit Index-Signatur gibt keyof den Indextyp zurück, und eine String-Index-Signatur ergibt string | number, nicht nur string:
JavaScript wandelt jeden numerischen Eigenschaftsschlüssel in einen String um, also sind scores[42] und scores["42"] dieselbe Eigenschaft. TypeScript bildet das ab, indem es number einschließt. Die Ausgabe zeigt außerdem eine Regel von JavaScript: Ganzzahlartige Schlüssel werden zuerst aufgelistet, aufsteigend sortiert, vor den übrigen String-Schlüsseln. Wenn eine generische Funktion nur String-Schlüssel braucht, schreibe Extract<keyof T, string> oder string & keyof T; Exclude und Extract haben eine eigene Seite.
Ergebnisse von keyof im Überblick
| Typ | keyof ergibt |
|---|---|
{ id: number; name: string } | "id" | "name" |
{ [key: string]: number } | string | number |
{ [index: number]: string } | number |
{} | never |
any | string | number | symbol |
{ a: 1; b: 2 } | { a: 3; c: 4 } | "a" (Schlüssel, die jedes Mitglied hat) |
{ a: 1 } & { c: 4 } | "a" | "c" (Schlüssel von beiden) |
string[] | number plus jeder Name einer Array-Methode ("length", "push", ...) |
Die Zeilen zu Union und Intersection wirken zuerst verkehrt herum. Ein Wert eines Union-Typs könnte jedes der Mitglieder sein, also sind nur Schlüssel sicher, die in beiden vorkommen. Ein Wert einer Intersection hat alles von beiden, also hat er alle Schlüssel.
Warum Object.keys string[] zurückgibt
Object.keys(obj) ist als string[] typisiert, nie als (keyof T)[]. Der Grund ist die strukturelle Typisierung: Ein Wert kann mehr Eigenschaften tragen als sein deklarierter Typ.
p ist als Point typisiert, hat aber zur Laufzeit einen Schlüssel z. Würde Object.keys ein ("x" | "y")[] versprechen, wäre Code, der sich darauf verlässt, für p falsch. Der Cast ist sicher für Objektliterale, die du selbst erzeugt hast; bei Objekten, die von außen kommen, behältst du die Schlüssel als string und prüfst sie.
keyof in Mapped Types
Mit keyof durchläufst du auch die Schlüssel eines Typs, um einen neuen Typ zu bauen. So sind die eingebauten Utility Types Partial und Readonly geschrieben:
type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
interface User {
id: number;
name: string;
}
type UserDraft = MyPartial<User>; // { id?: number; name?: string }
[K in keyof T] besucht jeden Schlüssel, und T[K] liest den Eigenschaftstyp unter diesem Schlüssel. Mapped Types haben eine eigene Seite mit Modifikatoren und Key Remapping.
Häufig gestellte Fragen
Was macht keyof in TypeScript?
keyof T erzeugt eine Union der Eigenschaftsnamen des Typs T. Für interface User { id: number; name: string } ist keyof User gleich "id" | "name". Es funktioniert nur mit Typen, beim Kompilieren, und erzeugt kein JavaScript.
Was ist keyof typeof in TypeScript?
keyof braucht einen Typ, aber ein einfaches Objekt wie const colors = { red: "#f00" } ist ein Wert. typeof colors macht aus dem Wert seinen Typ, und keyof typeof colors liefert dann die Union seiner Schlüssel, hier "red". keyof colors scheitert mit dem Fehler TS2749.
Warum gibt keyof string | number zurück?
Wenn der Typ eine String-Index-Signatur wie { [key: string]: number } hat. JavaScript wandelt numerische Eigenschaftsschlüssel in Strings um, also ist obj[42] auch ein gültiger Zugriff, und TypeScript nimmt number in den Schlüsseltyp auf. Nimm Extract<keyof T, string> oder string & keyof T, wenn du nur die String-Schlüssel willst.
Warum gibt Object.keys string[] statt (keyof T)[] zurück?
Weil ein Objekt mehr Eigenschaften haben kann, als sein Typ auflistet: Ein Wert mit einem zusätzlichen Feld lässt sich einem engeren Typ zuweisen. Das Ergebnis als (keyof T)[] zu typisieren, wäre für solche Objekte eine Lüge. Wenn du weißt, dass das Objekt genau die deklarierten Schlüssel hat, caste: Object.keys(obj) as (keyof typeof obj)[].
Wie behebe ich "Element implicitly has an 'any' type because expression of type 'string' can't be used to index type"?
Der Index ist ein einfacher string, und der Objekttyp hat keine String-Index-Signatur. Typisiere den Parameter als keyof YourType statt als string, oder prüfe den String vorher mit einem Type Guard, der ihn auf keyof YourType eingrenzt, oder gib dem Objekt eine Index-Signatur oder den Typ Record<string, V>, wenn es wirklich jeden Schlüssel akzeptiert.