keyof and indexed access
What you'll learn:
- get a type's property names with
keyofand a property's type withUser["age"]; - write a function that accepts only real property names and returns the exact type;
- take the type of an existing value with
typeofand turn a list of values into a union; - build new types from old ones with the utilities
Partial,Pick,Omit,Recordand others.

Picture a form generator. You describe a form once: which fields it has and what goes into each. From then on the generator makes new papers from it: a list of field names, a draft where any field may stay empty, a short card with two fields. Nothing has to be copied by hand, and a new field on the form shows up on every paper.
Type operators in TypeScript work the same way: they build new types from types you have already described. Let's start with the first paper, the list of field names. Here is a task that needs it: a function gets a user and the name of the field to show.
interface User {
name: string;
age: number;
}
function getField(user: User, field: string) {
return user[field]; // ✗
}error TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'User'.tsc refuses to read user[field]. A parameter of type string may receive any string: "email", "nmae". User has no such properties, and the compiler does not know what the read returns. And strict mode does not allow it to quietly treat the result as any.
You need a type that contains only the property names of User.
The keyof operator
The keyof operator takes an object type and returns the union of its property names. For our interface, keyof User is "age" | "name": a union of two literal types, one per property. You can write it wherever a type goes, for example on a parameter:
interface User {
name: string;
age: number;
}
function getField(user: User, field: keyof User) {
return user[field];
}
const ann: User = { name: "Ann", age: 30 };
console.log(getField(ann, "name")); // Ann
console.log(getField(ann, "age")); // 30Let's walk through the example line by line:
field: keyof Userallows the parameter only two values:"name"and"age".user[field]compiles now: whichever name arrives,Userhas that property.- Both calls pass real names and print
Annand30.
tsc does not let through a name that User lacks. The call getField(ann, "email") is an error:
error TS2345: Argument of type '"email"' is not assignable to parameter of type 'keyof User'.Two things about how TypeScript shows such types:
- the editor's hover and the error messages keep the type as it was written:
keyof User, not a list of names; - when TypeScript 7 prints a union of literals, it sorts the members, so
"age"comes before"name"even though the interface declaresnamefirst.
The order of the members in a union means nothing: "age" | "name" and "name" | "age" are the same type.
Here the form generator stops being like keyof. You can hold a paper list of fields in your hands, while keyof User exists only for tsc and is erased from the JavaScript. At run time you can learn the property names only from a real object, for example with Object.keys.
A property's type by name
Square brackets work in types too. User["age"] is an indexed access type: the type of the property age of the type User. In code ann["age"] gets a property's value, and in a type User["age"] gets its type:
User["age"]isnumber;User["name"]isstring;User[keyof User]isstring | number: the brackets hold a union of names, so you get a union of the types of all the properties;User["email"]does not compile:error TS2339: Property 'email' does not exist on type 'User'.
Now it is clear what getField from the example above returns. Any name from keyof User may arrive in user[field], so the result has the type User[keyof User], which is string | number. The call getField(ann, "name").toUpperCase() does not compile: the function does not remember that it was given "name" in particular.
The exact type in a generic function
To make the function remember the name, you turn the name into a type parameter:
interface User {
name: string;
age: number;
}
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const ann: User = { name: "Ann", age: 30 };
console.log(getProp(ann, "name").toUpperCase()); // ANN
console.log(getProp(ann, "age").toFixed(1)); // 30.0Let's take the signature apart:
Tis the type of the object,Kthe name of the property.K extends keyof Tis a constraint, as in the previous theme:Kmay only be one of the property names ofT.T[K]is the result type: the type of the property whose name was passed.- In the call
getProp(ann, "name")TypeScript infersTasUserandKas"name". The result isUser["name"], which isstring, andtoUpperCaseworks. - For
"age"the same function returnsnumber, andtoFixedworks.
Step through the example. In the first function the hover reveals keyof User by narrowing, and in the second it shows which type the result of getProp gets:
1interface User { name: string; age: number }2 3function label(key: keyof User) {editor hoverkey: keyof User4 if (key === "name") {5 return "Full " + key;6 }7 return "Your " + key;8}9 10function getProp<T, K extends keyof T>(11 obj: T,12 key: K,13): T[K] {14 return obj[key];15}16const u: User = { name: "Ann", age: 30 };17const n = getProp(u, "name");18const a = getProp(u, "age");
The hover shows the type as it was written: keyof User.
Interviewers like to ask you to write a function like getProp, one that reads a property by name and keeps its exact type. The solution is a second type parameter K extends keyof T and the result T[K].
Common mistake
The second type parameter is left out, and the property name is typed directly as keyof T:
interface User {
name: string;
age: number;
}
function getProp<T>(obj: T, key: keyof T): T[keyof T] {
return obj[key];
}
const ann: User = { name: "Ann", age: 30 };
getProp(ann, "name").toUpperCase(); // ✗error TS2339: Property 'toUpperCase' does not exist on type 'string | number'.Once again the function does not remember which name it was given. T[keyof T] is the union of the types of all the properties, string | number, and a number has no toUpperCase method. The fix is shown above: the name becomes a type parameter of its own, K, and the result becomes T[K].
Key points
keyof Useris the union of the property names ofUser:"age" | "name".User["age"]is a property's type by name,number;User[keyof User]is the union of the types of all the properties.<T, K extends keyof T>(obj: T, key: K): T[K]accepts only real names and returns the type of exactly that property.- Only tsc checks
keyofandT[K]: none of it is left in the JavaScript.