Marius Schulz
Marius Schulz
Software Engineer

keyof and Lookup Types in TypeScript

JavaScript is a highly dynamic language, and it can be tricky to capture the semantics of certain operations in a static type system. Take a simple prop function, for instance:

function prop(obj, key) {
  return obj[key];
}

It accepts an object and a key and returns the value of the corresponding property. Different properties can have totally different types, and we don't even know what obj looks like.

So how could we type this function in TypeScript? Here's a first attempt:

function prop(obj: {}, key: string) {
  return obj[key];
}

With these two type annotations in place, obj must be an object and key must be a string. However, the return type is still inferred as any:

const todo = {
  id: 1,
  text: "Buy milk",
  due: new Date(2016, 11, 31),
};

const id = prop(todo, "id"); // any
const text = prop(todo, "text"); // any
const due = prop(todo, "due"); // any

Without further information, TypeScript can't know which key will be passed, so it can't infer a more specific return type for the prop function. We need to provide a little more type information.

The keyof Operator #

Enter TypeScript 2.1 and the new keyof operator. It queries the set of keys of a given type, which is why it's also called an index type query. Let's assume we've defined the following Todo interface:

interface Todo {
  id: number;
  text: string;
  due: Date;
}

We can apply the keyof operator to the Todo type to get back a type representing all its property keys, which is a union of string literal types:

type TodoKeys = keyof Todo; // "id" | "text" | "due"

We could've written out the union type "id" | "text" | "due" by hand, but that would've been cumbersome, error-prone, and a nightmare to maintain — and it would've been specific to the Todo type rather than generic.

Indexed Access Types #

Equipped with keyof, we can now improve the type annotations of our prop function. Instead of accepting arbitrary strings for the key parameter, we'll require that the key actually exists on the type of the object passed in:

function prop<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

TypeScript now infers the prop function to have the return type T[K], an indexed access type (also called a lookup type) that represents the type of the property K of the type T. If we now access the three todo properties via the prop function, each one has the correct type:

const todo = {
  id: 1,
  text: "Buy milk",
  due: new Date(2016, 11, 31),
};

const id = prop(todo, "id"); // number
const text = prop(todo, "text"); // string
const due = prop(todo, "due"); // Date

Now, what happens if we pass a key that doesn't exist on the todo object?

Invalid key

The compiler complains, and that's a good thing! It prevented us from trying to read a property that's not there.

For another real-world example, check out how the Object.entries() method is typed in the lib.es2017.object.d.ts type declaration file that ships with the TypeScript compiler:

interface ObjectConstructor {
  // ...
  entries<T extends { [key: string]: any }, K extends keyof T>(
    o: T,
  ): [keyof T, T[K]][];
  // ...
}

The entries method returns an array of tuples, each containing a property key and the corresponding value. There are plenty of square brackets involved, admittedly, but there's the type safety we've been looking for!

This article and 44 others are part of the TypeScript Evolution series. Have a look!