#The keyof operator
keyof T takes an object type and gives you the union of its keys as literal types. For interface Person { name: string; age: number }, keyof Person is "name" | "age". It's the type-level cousin of Object.keys — but it works on the type, at compile time, and nothing of it exists at runtime.
It exists so you can write code that is generic over "some property of this object" without falling back to string. A parameter typed keyof Person accepts exactly the valid property names, gets autocompletion, and breaks at compile time when someone renames a property. Combined with generics it gives the type-safe getValue; combined with in it drives every mapped type.
A mental model: keyof answers the question "which keys can I safely use to index this type?". That phrasing explains all the surprising cases below — index signatures, unions, arrays and {}.
interface Person {
name: string;
age: number;
}
type PersonKey = keyof Person; // type PersonKey = "name" | "age"
function printProp(p: Person, key: PersonKey) {
console.log(p[key]);
}
const ann: Person = { name: "Ann", age: 30 };
printProp(ann, "age"); // 30
printProp(ann, "email"); // ❌ Argument of type '"email"' is not assignable to parameter of type 'keyof Person'.keyof Personlooks at the declared members ofPerson—nameandage— and produces one string literal type per key, joined into a union:"name" | "age".PersonKeyis just a name for that union. The checker usually displays it askeyof Personin messages and as the expanded union on hover.printProp(ann, "age")checks"age"against"name" | "age"— fine — and at runtimep["age"]logs30."email"isn't in the union, so the call fails with TS2345. If someone later renamesagetoyearsin the interface, every"age"argument becomes a compile error instead of a silentundefined.- Inside the function,
p[key]has the typePerson[PersonKey]=string | number— an indexed access type.
keyof edge cases with index signaturesThe edge cases all follow from "which keys can I safely index with":
type Dict = { [key: string]: boolean };
type DictKeys = keyof Dict; // type DictKeys = string | number
type NumDict = { [index: number]: boolean };
type NumDictKeys = keyof NumDict; // type NumDictKeys = number
type Anything = keyof any; // type Anything = string | number | symbol
type Nothing = keyof {}; // type Nothing = never
type Common = keyof ({ a: 1; b: 2 } | { a: 3; c: 4 }); // type Common = "a"
type All = keyof ({ a: 1 } & { c: 4 }); // type All = "a" | "c"
const id = Symbol("id");
type Mixed = keyof { [id]: number; name: string; 0: boolean }; // typeof id | "name" | 0
type ArrayKeys = keyof string[]; // number | "length" | "push" | "pop" | … every array member
const k: ArrayKeys = "length";
const i: ArrayKeys = 3;- String index signature:
{ [key: string]: boolean }can be indexed by any string — and by any number, because JavaScript convertsobj[0]toobj["0"]. Sokeyofgivesstring | number. This is a classic surprise when you expect plainstring(see Index signatures). - Number index signature: only numbers are promised, so
keyofisnumber. keyof anyis every type that can be a property key:string | number | symbol— the same as the built-inPropertyKey(see PropertyKey and keyof any).keyof {}isnever: the empty object type promises no keys at all.- Union: a value of type
A | Bmight be either, so you can only safely use keys that both have.keyof (A | B)is the intersection of the key sets: just"a". Intersection: a value of typeA & Bhas everything from both, sokeyof (A & B)is the union of the key sets:"a" | "c". The operators flip — worth saying out loud in an interview. - Symbol and numeric keys keep their own types: a computed
[id]key contributestypeof id(aunique symbol), and the numeric literal key0contributes the number literal0, not"0". - Arrays:
keyof string[]isnumber(the index signature) plus the name of every array property and method —"length","push","map"and so on. That's whykeyofon arrays is rarely what you want; useT[number]for the element type instead.
keyof vs runtime keyskeyof gets usedWhere you'll use it: function parameters that must be a property name (sortBy(users, "age")), the K extends keyof T constraint, and mapped types ({ [K in keyof T]: … }), which is how Partial<T>, Readonly<T> and Pick<T, K> are built.