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 Tortypeof 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
symbolvalue instead of text.keyofincludes 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.
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 menu02Why 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.
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"); // error04Index 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).
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
Grow the type
In the first example add
phone: string;toUser. Predict: doesshow("phone")become legal without any other edit? Try it.Then change
UserKeyto a hand-written union of the three old names. AddphonetoUseragain and notice nothing warns you that the list is out of date. - 2
Ask for the opposite of a menu
In the third example, change
keyof (Cat | Dog)tokeyof Catand read the error: the message now saysType 'keyof Cat' is not assignable to type 'never'and underneath names"lives". Change it back, then addlives: numbertoDog. Predict the new answer forkeyof (Cat | Dog)first: it is now"lives" | "name", because both types have those two keys. - 3
Break the constraint
In the second example change
K extends keyof TtoK. Two errors appear, bothTS2536: Type 'K' cannot be used to index type 'T': one onT[K]in the return type (line 1) and one onobj[key](line 2). Why can't the compiler allow it? Hovertitle: without the link betweenKandT, the result is nowunknown.
Code & diagrams
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: ageEach 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 trueAssigning 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)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 AshaBreak 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.
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.
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 Useris worked out immediately into the union of literals. The namekeyof Userin 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 showkeyof 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 genericT,keyof Tcan't be worked out, so it stays symbolic and the checker reasons with rules:K extends keyof Tallowsobj[key], andT[K]is the property type. - ▸
keyof Tfor a type with astringindex signature includesnumberbecause of the JavaScript key-coercion rule, and a numeric-looking key such as{ 0: "a" }gives the literal type0, 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
The problem this solves: a list of valid key names, typed by hand, goes stale the day someone adds or renames a field. **
keyof Tis a type operator**: it takes a typeTand gives back a new type. The result is a union (a type that means "one of these") made of the key names ofT, each one as a string literal type (a type that allows exactly one piece of text). For{ id: number; name: string; email: string },keyofis"id" | "name" | "email". A variable of that type can hold"name"but not"age". - 2
The point is to stop typing key lists by hand. If you write
type Key = "id" | "name" | "email"next toUser, the day someone addsphonetoUserthe two lists disagree and nothing warns you. Withkeyof Userthere is only one list, and it isUseritself. - 3
keyofshines 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 whatKmay be.K extends keyof Treads "K must be one of T's key names". Pass"price"for an object with nopriceand the call is an error. TheT[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
Index signatures change the answer. An index signature says "any key of this kind is allowed" (Topic 3.6).
keyof { [key: string]: number }isstring | number, not juststring, because JavaScript turns every number key into text anyway:obj[0]andobj["0"]read the same property, so TypeScript accepts both. With a number index signature,keyofis justnumber. - 5
Objects can also have symbol keys (a
symbolis a unique value made withSymbol(), often used as a hidden property name).keyofincludes them, as the special typetypeof idfor a symbol stored in a constantid(thetypeofhere is the type-world one that Topic 8.2 teaches: "the type of the valueid"). The widest case iskeyof any, which isstring | number | symbol: everything JavaScript allows as a property key. That is why a generic key parameter often needsstring & Kbefore you build text from it (you'll meet that in Topic 8.5). - 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 typeA | Bmight be either one, and you may only touch what both guarantee.keyof (A & B)gives the keys of either, because a value of typeA & Bis both at the same time. Think of the union of keys following the opposite of the union of types. - 7
keyofis only about types, and types are erased when the program runs. At runtime you still useObject.keys(obj), and TypeScript types that asstring[], 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 withas, but then you are making the promise, not TypeScript.
Explain it without notes
What does keyof T return, and why is it useful?
Why does keyof (A | B) give fewer keys than keyof (A & B)?
What does keyof { [k: string]: number } return, and why isn't it just string?
Why does Object.keys(obj) return string[] instead of (keyof typeof obj)[]?
Practice
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".
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
- ↔
keyofkeeps lists in sync automatically, but the resulting error messages can mentionkeyof Somethinginstead 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 Userreturns for a given object type.I can write
K extends keyof Tand say what it limits.I can explain why
keyofon a string index signature includesnumber.I can say which keys
keyof (A | B)andkeyof (A & B)keep.I can explain why
Object.keysreturnsstring[].