Command Palette

Search for a command to run...

Hectal
PHASE 8Intermediate ~24 min· topic 1 of 5

Topic 8.1

The keyof Operator

In one line

keyof T turns an object type into a union of its key names, so you can say "any valid key of this type" and let the compiler keep that list up to date for you.

Think of it like this

A restaurant menu lists every dish you are allowed to order. If you ask the waiter for "purple elephant soup", they say it isn't on the menu. **keyof prints the menu of an object type**: the list of names you may ask for. When the kitchen adds a dish, the menu updates itself, and nobody has to retype it.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Type operator
A piece of syntax that takes one or more types and produces a new type, like keyof T or typeof value.
keyof
The operator that gives the key names of a type as a union of string literal types.
Union
A type that means "one of these", written with |, like "a" | "b".
String literal type
A type that allows exactly one piece of text, like "name".
Generic
A function or type with a placeholder for a type, written in angle brackets, like <T>.
Constraint
A limit on a generic placeholder, written K extends keyof T: K can be any type that fits inside the limit.
Index signature
A rule like { [key: string]: number } that allows any key of that kind.
Symbol key
A property whose name is a symbol value instead of text. keyof includes it.
Source of truth
The one place a fact is written, so every other place derives from it and can't drift apart.

Step by step

01The menu printed from the object

Start with a User type. Asking for keyof User gives the union of its three field names.

You can use that type anywhere a type goes: a parameter, a variable, a return type. Here show only accepts a real field name.

keys.tswhole filets
type User = { id: number; name: string; email: string };
type UserKey = keyof User; // "id" | "name" | "email"

function show(key: UserKey) {
  console.log("key:", key);
}
show("name");  // fine
show("age");   // error TS2345: not on the menu

02Why not write the union by hand?

A hand-written "id" | "name" | "email" is a second copy of the same fact. The two copies agree today and drift tomorrow. keyof User can't drift, because it is read from User.

03Pair keyof with a generic

The classic use is a function that reads a property by name. The constraint K extends keyof T stops you naming a key that doesn't exist, and the return type T[K] gives back exactly the type of that property: string for "title", number for "pages".

K is inferred from the argument you pass. Because "title" is a literal, K becomes "title", not string.

get-prop.tswhole filets
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

getProp({ title: "Dune", pages: 412 }, "pages"); // number
getProp({ title: "Dune", pages: 412 }, "price"); // error

04Index signatures widen the answer

For { [key: string]: number }, keyof is string | number. JavaScript property names are always text underneath, so dict[0] is the same as dict["0"], and TypeScript allows number keys too.

For { [index: number]: string } (an array-like object), only number is allowed.

05Unions and intersections swap the rule

A value of type Cat | Dog is one or the other, so you may only use what both have. A value of type Cat & Dog is both at once, so you may use everything either has.

The picture shows the keys of two types. The overlap is what keyof (Cat | Dog) keeps. Everything in the circles together is keyof (Cat & Dog).

Unions and intersections swap the rulediagram
Rendering diagram…

06Runtime keys are a separate thing

Object.keys(user) runs when the program runs and returns string[]. TypeScript does not narrow it to (keyof User)[], because a value can have extra properties its type doesn't mention.

If you know the object is exactly its type, write Object.keys(user) as Array<keyof typeof user>. That as is your promise, so use it only when it's true.

Try it yourself

  1. 1

    Grow the type

    In the first example add phone: string; to User. Predict: does show("phone") become legal without any other edit? Try it.

    Then change UserKey to a hand-written union of the three old names. Add phone to User again and notice nothing warns you that the list is out of date.

  2. 2

    Ask for the opposite of a menu

    In the third example, change keyof (Cat | Dog) to keyof Cat and read the error: the message now says Type 'keyof Cat' is not assignable to type 'never' and underneath names "lives". Change it back, then add lives: number to Dog. Predict the new answer for keyof (Cat | Dog) first: it is now "lives" | "name", because both types have those two keys.

  3. 3

    Break the constraint

    In the second example change K extends keyof T to K. Two errors appear, both TS2536: Type 'K' cannot be used to index type 'T': one on T[K] in the return type (line 1) and one on obj[key] (line 2). Why can't the compiler allow it? Hover title: without the link between K and T, the result is now unknown.

Code & diagrams

keyof gives the menu of an object type

The Types panel shows UserKey and the error for "age". The code still runs (like tsc, it emits anyway), so the console prints key: age: TypeScript warned about it, JavaScript let it through. Add phone: string to User and watch nothing else need changing.

Console

(nothing was printed)

Expected console output

key: name
key: email
key: age
keyof with a generic: read a property by name

Each call returns the exact type of the property you named. The error shows the real menu of book: the three key names, in the order the compiler stores them. either() can pick two keys, so its result is the union of both property types.

Console

(nothing was printed)

Expected console output

Dune 412 true
Index signatures, symbols, unions and intersections

Assigning to never is a trick that makes tsc print the type: it can never fit, so the error message spells out what it was. Notice the union Cat | Dog keeps only name, while Cat & Dog keeps all three.

Console

(nothing was printed)
Object.keys returns string[], not (keyof T)[]

The first loop is an error because k is a plain string, and a string can't index an object that has only id and name. The second loop works because the cast promises the keys are exactly id and name.

Console

(nothing was printed)

Expected console output

7
Asha
id 7
name Asha

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Name a key that isn't there

In the second example, look at the last line: getProp(book, "price"). Change it to getProp(book, "pages") and the error disappears; change it back to see it return.

terminal
$ Types panel
── what you'll see ──
index.ts(16,15): error TS2345: Argument of type '"price"' is not assignable to parameter of type '"inStock" | "pages" | "title"'.

Break #2

Use runtime keys as if they were typed keys

In the last example, delete the as Array<keyof typeof user> cast from the second Object.keys line. The first loop already shows this error (line 6); now the second loop gets it too.

terminal
$ Types panel
── what you'll see ──
index.ts(11,18): error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ id: number; name: string; }'.
No index signature with a parameter of type 'string' was found on type '{ id: number; name: string; }'.

Myth vs fact

Myth

keyof gives the keys of an object at runtime.

Fact

It only exists in the type world and vanishes when the code runs. For runtime keys you use Object.keys, which TypeScript types as string[].

Myth

keyof on an object with a string index signature is string.

Fact

It is string | number, because JavaScript turns number keys into text and so reading obj[0] is allowed.

Myth

keyof (A | B) is all the keys of A and B.

Fact

It's only the keys both have. The keys of a union are the overlap, and the keys of an intersection are everything.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    For a concrete type, keyof User is worked out immediately into the union of literals. The name keyof User in hover text and errors is a label the checker remembers for unions that came from a class, an interface or a named alias (this course's examples show keyof Tagged, keyof WithBoth). An inline object type has no name to remember, so the second example's error prints the expanded "inStock" | "pages" | "title". For a generic T, keyof T can't be worked out, so it stays symbolic and the checker reasons with rules: K extends keyof T allows obj[key], and T[K] is the property type.

  • ▸

    keyof T for a type with a string index signature includes number because of the JavaScript key-coercion rule, and a numeric-looking key such as { 0: "a" } gives the literal type 0, not "0" (keyof { 0: "a"; x: 1 } is "x" | 0).

  • ▸

    Union members are printed in the compiler's own order, not the order you wrote them: the object in the second example lists title, pages, inStock, and the error prints "inStock" | "pages" | "title". TypeScript 7 always uses the stable ordering that 6.0 offers with --stableTypeOrdering, so the order does not change between runs or machines. Don't write code or tests that depend on it.

Remember this

  1. 1

    The problem this solves: a list of valid key names, typed by hand, goes stale the day someone adds or renames a field. **keyof T is a type operator**: it takes a type T and gives back a new type. The result is a union (a type that means "one of these") made of the key names of T, each one as a string literal type (a type that allows exactly one piece of text). For { id: number; name: string; email: string }, keyof is "id" | "name" | "email". A variable of that type can hold "name" but not "age".

  2. 2

    The point is to stop typing key lists by hand. If you write type Key = "id" | "name" | "email" next to User, the day someone adds phone to User the two lists disagree and nothing warns you. With keyof User there is only one list, and it is User itself.

  3. 3

    keyof shines together with generics (a type that takes a type as input, Topic 7.1): function getProp<T, K extends keyof T>(obj: T, key: K): T[K]. Recall from Topic 7.3: K extends ... is a constraint, a limit on what K may be. K extends keyof T reads "K must be one of T's key names". Pass "price" for an object with no price and the call is an error. The T[K] in the return type is an indexed access, the subject of Topic 8.3: it means "the type of the property called K".

  4. 4

    Index signatures change the answer. An index signature says "any key of this kind is allowed" (Topic 3.6). keyof { [key: string]: number } is string | number, not just string, because JavaScript turns every number key into text anyway: obj[0] and obj["0"] read the same property, so TypeScript accepts both. With a number index signature, keyof is just number.

  5. 5

    Objects can also have symbol keys (a symbol is a unique value made with Symbol(), often used as a hidden property name). keyof includes them, as the special type typeof id for a symbol stored in a constant id (the typeof here is the type-world one that Topic 8.2 teaches: "the type of the value id"). The widest case is keyof any, which is string | number | symbol: everything JavaScript allows as a property key. That is why a generic key parameter often needs string & K before you build text from it (you'll meet that in Topic 8.5).

  6. 6

    With unions the rule flips, and it trips up many people. keyof (A | B) gives only the keys both types have, because a value of type A | B might be either one, and you may only touch what both guarantee. keyof (A & B) gives the keys of either, because a value of type A & B is both at the same time. Think of the union of keys following the opposite of the union of types.

  7. 7

    keyof is only about types, and types are erased when the program runs. At runtime you still use Object.keys(obj), and TypeScript types that as string[], not (keyof T)[]. This is deliberate. An object can have more properties at runtime than its type says, because a value with extra fields is still assignable to the smaller type. So a list of runtime keys can't safely be trusted to match the type's keys. When you know the object has no extras you can cast with as, but then you are making the promise, not TypeScript.

Explain it without notes

01

What does keyof T return, and why is it useful?

02

Why does keyof (A | B) give fewer keys than keyof (A & B)?

03

What does keyof { [k: string]: number } return, and why isn't it just string?

04

Why does Object.keys(obj) return string[] instead of (keyof typeof obj)[]?

Practice

01

Write function pluck<T, K extends keyof T>(items: T[], key: K): T[K][] that returns the list of one field from a list of objects. Call it on [{ id: 1, name: "A" }, { id: 2, name: "B" }] for "name" and for "price".

02

Write a type Status = keyof typeof statusLabels for the object const statusLabels = { open: "Open", closed: "Closed" } and a function that takes a Status and returns its label. What does it accept after you add archived: "Archived" to the object?

Trade-offs

  • ↔

    keyof keeps lists in sync automatically, but the resulting error messages can mention keyof Something instead of the three names you expected. Hover the alias or use the never trick to see the expanded list.

  • ↔

    A cast like Object.keys(x) as Array<keyof typeof x> is convenient but it's a promise the compiler can't check. Keep it in one helper so a wrong promise lives in one place.

Done when you can

  • I can say what keyof User returns for a given object type.

  • I can write K extends keyof T and say what it limits.

  • I can explain why keyof on a string index signature includes number.

  • I can say which keys keyof (A | B) and keyof (A & B) keep.

  • I can explain why Object.keys returns string[].