27 April, 2023 | 4 min read

Why Object.keys Returns an Array of Strings in TypeScript (And How To Fix It)

One of the most common pain points I see when people first try TypeScript is when they call Object.keys. Their code might look something like this:

const zuko = {
  nationality: 'Fire Nation',
  nickname: 'Zuzu'
};

Object.keys(zuko).forEach(key => {
  console.log(zuko[key]);
});

TypeScript playground

This looks like a basic use case for Object.keys and yet TypeScript complains.

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ nationality: string; nickname: string; }'.

Closer inspection reveals the source of the problem. Our key variable is type string and TypeScript is warning us that our zuko object has specific values for the keys, not just any string.

“But wait”, you might think, “if we’re looping through the keys of an object, then we should be able to index the object with those keys. Object.keys shouldn’t return string[], it should return (keyof typeof obj)[]“.

Text children of block elements are treated as an anonymous inline element

It turns out though that there’s a good reason why TypeScript returns the type of string[] for Object.keys. Let’s take a look at why TypeScript behaves in this way and then look at how we can solve the type error.

Why Object.keys Returns An Array Of Strings

TypeScript’s types are a lie. They’re a useful lie, but they’re still a lie. Objects are build time can contain keys at runtime which are not included in the type definition.

Consider the following code:

type User = {
  username: string;
  email: string;
}

function getUser(): User {
  const user = {
    username: 'bob',
    email: 'bob@bobby.com',
    password: 'PLS_DO_NOT_SHARE',
  };

  return user;
}

TypeScript Playground

This is valid TypeScript code. getUser will return the type User which only has the keys username and email. However, at runtime we’ll be returning an additional key, password.

Now when we call Object.keys(getUser()), we will get three keys, username, email, and password. This would mean a type definition of keyof typeof user (username | email) would be incorrect.

To help prevent problems that can come from this, TypeScript says “I can’t guarentee that Object.keys will return (typeof keyof obj)[]. The best I can say with certainty is that they will be an array of strings”.

Solving The Object.Keys Type Error

That’s all well and good, but looping over an object’s keys and using them to access the object’s values is a really common action in JavaScript. Are we saying it’s just not possible in TypeScript?

Not exactly. We just need to show TypeScript that the keys are valid keys within the object.

Solution 1: as keyof typeof obj

The easiest way around this is to just use type assertions to force TypeScript to be satisfied.

const zuko = {
  nationality: 'Fire Nation',
  nickname: 'Zuzu'
};

Object.keys(zuko).forEach(key => {
  console.log(zuko[key as keyof typeof zuko]);
});

TypeScript Playground

The rational is that we know key will be a valid key of our object, so using that key to access the value should not be a problem, even if the type keyof typeof obj may not be strictly accurate.

Although this gets the job done, you do sacrifice some of the type safety that TypeScript gives you. Object.key returns string[] for a reason. But in most cases, this is good enough.

If you find yourself having to do this often, you may want to use Matt Pocock’s custom objectKeys function which handles this for you.

Solution 2: Object.values and Object.entries

Most of the time this problem happens because you are trying to use the keys to access the values of the same object. In this case, you can just use Object.values if you just need the object values, or Object.entries if you need the key and values.

For our example above, instead of using Object.keys to log all the object values, we could use Object.values:

const zuko = {
  nationality: 'Fire Nation',
  nickname: 'Zuzu'
};

Object.values(zuko).forEach(value => {
  console.log(value);
});

TypeScript Playground

Solution 3: Create a Type Guard

If we want to keep maximum type safety and we can’t use Object.values or Object.entries, then we can solve the problem by creating a user defined type guard.

Our type guard will take a string and test whether the value is a valid key of the object. If it is, we’ll return true, otherwise false.

The magic comes in the is keyword in the return statement. This tells TypeScript that if we have checked this function returns true then the value passed in can be typed as keyof typeof obj.

const zuko = {
  nationality: 'Fire Nation',
  nickname: 'Zuzu'
};

function isZukoKey(value: string): value is keyof typeof zuko {
  return Object.keys(zuko).includes(value);
}

Object.keys(zuko).forEach(key => {
  if (isZukoKey(key)) {
    console.log(zuko[key]);
  }
});

TypeScript Playground

Because we are explicitly testing that the value is a key of our object, TypeScript is satisfied within that if statement for key to be of type keyof typeof obj.

This solves our problem while maintaining maximum type safety.


Follow me on Twitter to get updates on new posts.