DEV Community

Sam Wight
Sam Wight

Posted on

Today in TypeScript: the keyof operator

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

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'.
  }
}
Enter fullscreen mode Exit fullscreen mode

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  raw `user[key]` endraw  as  raw `any` endraw

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'.
Enter fullscreen mode Exit fullscreen mode

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  raw `key` endraw  is  raw `string` endraw .

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'.
Enter fullscreen mode Exit fullscreen mode

An index signature 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:

interface UsersTable {
  [id: string]: User;
}
Enter fullscreen mode Exit fullscreen mode

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).

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:

type UserKey = "name" | "email" | "password";
Enter fullscreen mode Exit fullscreen mode

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

  const keys: Array<UserKey> = ["name", "email", "password"];
Enter fullscreen mode Exit fullscreen mode

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.

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 keyofoperator

Here's the documentation for the keyof operator in the TypeScript handbook. 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:

type UserKey = keyof User
Enter fullscreen mode Exit fullscreen mode

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

Discussion (0)