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]);
});
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)[]
“.
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;
}
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]);
});
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);
});
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]);
}
});
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.