Course outline · 0% complete

0/26 lessons0%

Course overview →

keyof and Record

lesson 7-2 · ~11 min · 22/26

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, Record cannot 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 a number, but a key seen for the first time really is undefined at runtime. So counts[w] = (counts[w] ?? 0) + 1 reads as: start from the current tally, or 0 the first time. That ?? is the nullish coalescing operator from lesson 4-4.
  • Printing counts["fox"] gives undefined despite the type claiming number. This is a known soft spot in indexed access, and it is why the ?? 0 is 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: 17800

What the closed key type guarantees

  • Record<Day, number> forces all three days to be present. Delete wed from the literal and the compiler reports the missing property.
  • No stray keys are allowed either, so a typo like weds is an error rather than a silently added fourth entry.
  • Property access such as steps.mon is fully typed as number, which is why the three values can be added without any checking.
  • Add a "thu" member to Day and every Record<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.