keyof: the property names as a type
keyof T is a union of T's property names as literal types:
interface Point { x: number; y: number; } // keyof Point is "x" | "y" function getCoord(p: Point, key: keyof Point): number { return p[key]; }
getCoord(p, "x") compiles, getCoord(p, "z") does not. In plain JavaScript, p[key] with a misspelled key silently returns undefined. With keyof, property access by name is checked like everything else. You already saw Pick<User, "id" | "name"> in the last lesson, that K is constrained with keyof behind the scenes.
Property access checked by name
interface Point { x: number; y: number; } function getCoord(p: Point, key: keyof Point): number { return p[key]; } console.log(getCoord({ x: 3, y: 7 }, "x")); console.log(getCoord({ x: 3, y: 7 }, "y"));
Output
3 7
Both calls compile because "x" and "y" are the only members of keyof Point, and both are supplied correctly.
Calling getCoord({ x: 3, y: 7 }, "z") fails with: argument of type '"z"' is not assignable to parameter of type 'keyof Point'. The error lists the allowed keys, so a misspelling tells you the correct spelling. Compare the plain JavaScript version, where p["z"] quietly evaluates to undefined and the bug surfaces much later as an arithmetic result of NaN.
Record: a typed lookup table
Record<K, V> builds an object type with keys K and values V. Pair it with a union of literals for an exhaustive table:
type Fruit = "apple" | "banana" | "cherry"; const stock: Record<Fruit, number> = { apple: 4, banana: 0, cherry: 12, };
Two guarantees at once: every fruit must appear (forget cherry and it will not compile), and no stray keys sneak in. Compare the JavaScript habit of plain object maps where a typo creates a new key instead of an error. Record<string, number> is also legal when the key set is open-ended.
An open-ended Record used as a tally
Here Record<string, number> counts word occurrences, with keys that are not known ahead of time.
const counts: Record<string, number> = {}; const words = ["cat", "dog", "cat", "bird", "cat"]; for (const w of words) { counts[w] = (counts[w] ?? 0) + 1; } console.log("cat: " + counts["cat"]); console.log("dog: " + counts["dog"]);
Output
cat: 3 dog: 1
Two things this example is honest about
- With an open key type like
string,Recordcannot demand that every key exists. That guarantee only comes with a closed union of literals, as in the fruit table above. - The type says
counts[w]is anumber, but a key seen for the first time really isundefinedat runtime. Socounts[w] = (counts[w] ?? 0) + 1reads as: start from the current tally, or0the first time. That??is the nullish coalescing operator from lesson 4-4. - Printing
counts["fox"]givesundefineddespite the type claimingnumber. This is a known soft spot in indexed access, and it is why the?? 0is not optional.
A closed Record as a fixed table
Day is a three-member literal union, and Record<Day, number> builds a table with exactly those three keys.
type Day = "mon" | "tue" | "wed"; const steps: Record<Day, number> = { mon: 4200, tue: 8000, wed: 5600 }; const total = steps.mon + steps.tue + steps.wed; console.log("total steps: " + total);
Output
total steps: 17800What the closed key type guarantees
Record<Day, number>forces all three days to be present. Deletewedfrom the literal and the compiler reports the missing property.- No stray keys are allowed either, so a typo like
wedsis an error rather than a silently added fourth entry. - Property access such as
steps.monis fully typed asnumber, which is why the three values can be added without any checking. - Add a
"thu"member toDayand everyRecord<Day, number>in the codebase starts failing to compile until it is filled in. The union is the single place the day list lives.
A closed Record with a missing key
Declaring const scores: Record<"alice" | "bob", number> = { alice: 3 } produces a compile error: property bob is missing.
With a closed union as the key type, Record demands every key. The object literal has to include bob, and the compiler stops you immediately rather than letting a half-filled table through.
The practical value is that no undefined ever reaches runtime. Code downstream can read scores.bob and do arithmetic on it without a check, because the type system already proved the entry exists. That is a very different situation from Record<string, number>, where the key set is open and every lookup might come back empty.