Skip to content
Theory · Type operators and utility types · lesson 1 of 8

keyof and indexed access

What you'll learn

What you'll learn:

  • get a type's property names with keyof and a property's type with User["age"];
  • write a function that accepts only real property names and returns the exact type;
  • take the type of an existing value with typeof and turn a list of values into a union;
  • build new types from old ones with the utilities Partial, Pick, Omit, Record and others.
A form with the fields name and age goes into the keyof machine, and only the field names come out

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.

TypeScript
interface User {
    name: string;
    age: number;
}

function getField(user: User, field: string) {
    return user[field]; // ✗
}
TEXT
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:

TypeScript
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")); // 30

Let's walk through the example line by line:

  1. field: keyof User allows the parameter only two values: "name" and "age".
  2. user[field] compiles now: whichever name arrives, User has that property.
  3. Both calls pass real names and print Ann and 30.

tsc does not let through a name that User lacks. The call getField(ann, "email") is an error:

TEXT
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 declares name first.

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"] is number;
  • User["name"] is string;
  • User[keyof User] is string | 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:

TypeScript
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.0

Let's take the signature apart:

  1. T is the type of the object, K the name of the property.
  2. K extends keyof T is a constraint, as in the previous theme: K may only be one of the property names of T.
  3. T[K] is the result type: the type of the property whose name was passed.
  4. In the call getProp(ann, "name") TypeScript infers T as User and K as "name". The result is User["name"], which is string, and toUpperCase works.
  5. For "age" the same function returns number, and toFixed works.

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:

Livekeyof and a property's typestep 1 of 6

The hover shows the type as it was written: keyof User.

Press "Next step": the hover shows a type on the highlighted line

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:

TypeScript
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(); // ✗
TEXT
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 User is the union of the property names of User: "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 keyof and T[K]: none of it is left in the JavaScript.