Marius Schulz
Marius Schulz
Front End Engineer

keyof and Lookup Types in TypeScript

JavaScript is a highly dynamic language. It can be tricky sometimes 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 on an object 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. We've now restricted the set of possible values for both parameters. The return type is still inferred to be any, however:

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 value will be passed for the key parameter, so it can't infer a more specific return type for the prop function. We need to provide a little more type information to make that possible.

#The keyof Operator

Enter TypeScript 2.1 and the new keyof operator. It queries the set of keys for a given type, which is why it's also called an index type query. Let's assume we have 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 also written out the union type "id" | "text" | "due" manually instead of using keyof, but that would've been cumbersome, error-prone, and a nightmare to maintain. Also, it would've been a solution specific to the Todo type rather than a generic one.

#Indexed Access Types

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

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

TypeScript now infers the prop function to have a return type of T[K], a so-called indexed access type or lookup type. It represents the type of the property K of the type T. If we now access the three todo properties via the prop method, each one will have 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 in the return type, 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!