TypeScript 2.1: keyof and Lookup Types

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.

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.

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 lookup type or indexed access 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!

Use the coupon code LAUNCHDAY for $10 off!

Learn React

1 Comment

Evert van Brussel

Hey Marius,

Thanks for these articles. I just started using TypeScript like 2 days ago, so I really know next to nothing about it. So I have a question and I hope you can answer it.

Do you think it would be possible to use this feature to make a property decorator more (type-)safe? I like using VueJS and Vuex (flux-like state-management for VueJS) and using a type-decorator I can write the following in TypeScript:

@Component
export default class Counter extends Vue {
    @state count: number
}

Which basically translates to the following compiled javascript:

export default Vue.extend({
    name: "Counter",
    computed: {
        count: {
            get() { return this.$store.state.count },
            set(newCount) { this.$store.state.count = newCount }
        }
    }
})

So I already heard someone say that property-decorators can't influence or detect the type of the properties they're decorating. However, I can imagine that they can at least check the spelling of the prop-names, by using this new keyof T feature in typescript. Is that right? Or am I wrong here?