Today in TypeScript: the keyof operator
Tags: typescript
Published: 2021-12-02 17:26:17

Sometimes when you're trying to index on an object TypeScript will give you an error like this:

```typescript
interface User {
  name: string;
  email: string;
  password: string;
}

function printUser(user: User) {
  const keys = ["name", "email", "password"];
  
  for (const key of keys) {
    console.log(user[key]) // Error! Element implicitly has 'any' type
    // because expression of type 'string' can't be used to index type 'User'.
    // No index signature with a parameter of type 'string' was found on type 'User'.
  }
}
```

Let's figure out what's going on here. 

## Breaking it down

First, if we break out `user[key]` into a variable and hover over the variable, we can see that it has the type `any`:

![Screenshot showing that TypeScript infers the type of `user[key]` as `any`](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/03c2fsfdazxy35z0hwtn.png)

Why is this happening? TypeScript tells us in the error. Let's look at the error again:

```
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'User'.
  No index signature with a parameter of type 'string' was found on type 'User'.(7053)
```

Starting with the first line, TypeScript tells us that `user[key]` has the type `any`. The reason why it's giving us this error is because `noImplicitAny` is turned on.

```
Element implicitly has 'any' type
```

Why is this happening? TypeScript tells us that it's because a `string` type can't be used to index on type `User`.

```
because expression of type 'string' can't be used to index type 'User'.
```

Where's this string type coming from? Well, we're using the variable `key` to index on `user`, so let's look at its inferred type:

![Screenshot showing the inferred type of `key` is `string`.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l9s4e1j68aoehdhyecvs.png)
 
Ah, that makes more sense! TypeScript thinks that `key` is of type `string`. This comes from the type of `keys`, which is `string[]`.

The reason why TypeScript doesn't allow us to index on `User` with type `string` is because it doesn't have an 'index signature':

```
No index signature with a parameter of type 'string' was found on type 'User'.
```

An [index signature](https://dmitripavlutin.com/typescript-index-signatures/) is a thing you can add to interfaces when you only know the key and value types of an object. It tells TypeScript to allow you to index on an object with much broader types. For example, this:

```typescript
interface UsersTable {
  [id: string]: User;
}
```

would tell TypeScript to allow us to index on `UsersTable` with anything that is of type `string`.

This is the important part: if we don't have an index signature, TypeScript will have a stricter default. **TypeScript will only allow us to index on an object if the variable we're using to index is assignable to the *union of the keys* of that object**. So in the case of `User`, TypeScript will only allow us to index with a variable assignable to the type `"name" | "email" | "password"`. ([You can read on type assignability here](https://www.typescriptlang.org/docs/handbook/type-compatibility.html)).

This means that we need `key` to have a type of `"name" | "email" | "password"`, or something narrower (a subset of that, for example `"name"` or `"email" | "password"`). How do we fix that?

## Fixing the problem: Solution One

The first way we can fix this is pretty simple. Let's write out the type that TypeScript wants:

```typescript
type UserKey = "name" | "email" | "password";
```

And let's tell it that `keys` is an array of `UserKey`s:

```typescript
  const keys: Array<UserKey> = ["name", "email", "password"];
```

This works! The error goes away and TypeScript is happy.

To make sure we understand why, let's go back over the inferred types of the variables. `keys` is of type `Array<UserKey>` (or `UserKey[]`), which means `key` is now of type `UserKey`. `UserKey` is shorthand for `"name" | "email" | "password"`. Because `"name" | "email" | "password"` is assignable to the union of the keys of `User`, TypeScript lets us index on it. And now `user[key]` doesn't have an `any` type and is of type `string`. [Here's a TypeScript playground where you can experiment with this](https://www.typescriptlang.org/play?ssl=10&ssc=1&pln=11&pc=1#code/JYOwLgpgTgZghgYwgAgKoGdrIN4FgBQyyIcAthAFzLphSgDmA3AURKXMADZU10hMtkABzjp0AdwD2UACY9aDZvgC+BAmACeQlBmgBpCBuQBeZACIS5M8gA+5th07W7ZkWKmyza-DACuIBDBgSRBhPjBdKAAKX0woKkiAShxBBBCaZABrQ3QqAEEoKDgNAB5Igw0APhNkAG0LMggzABp7di4W8zcJaRkzAF0lIkEYaWQotJAM7KNJGCyc5LxCImRJjIA3OE5fFFNY6FqZ-sEidclOCAA6Tkl6GLijw37kgHpX5ABRQukAQi-LuRwMhgKQhJxgAhgGBOEYABaiZAAcjgIA0SOQmm0p2Q72QACMIAg4AdkBAAB5CKAQMTBUJzTFaFBI3gMDHEkBIsAElAHGSYyQgkAyCmM7TIyJIq44vEAOUFoBF5OowHoJDAvmpyHE0LhyDgwjgRXIkCgyAZWOZrP4GPEiNG-n5ITFzMl0pWqhUQA).

Yay, we fixed the error! But there's one small problem: what if we add keys to `User`? What happens then? Well we'd have to update the `UserKey` union with the new keys we added. There's also the potential to misspell a key or forget to add one, which TypeScript will scream at us for.

Fortunately, there's a way around this. The `keyof` operator.

## Solution two: The `keyof `operator

[Here's the documentation for the `keyof` operator in the TypeScript handbook](https://www.typescriptlang.org/docs/handbook/2/keyof-types.html). The docs tell us that `keyof` does this:

> The `keyof` operator takes an object type and produces a string or numeric literal union of its keys.

So this does exactly the same thing as what TypeScript does under the hood for indexing! `keyof` takes an interface / object and returns the union of all of the keys in it. So for our `User` interface, it would return `"name" | "email" | "password"`.

We can now use this to shorten our `UserKey` definition:

```typescript
type UserKey = keyof User
```

This works exactly the same as before, but now if we update `User`, we don't have to touch `UserKey`. Pretty great!