All Articles

How to Iterate Over Object Keys in TypeScript

Matt Pocock
Matt PocockMatt is a well-regarded TypeScript expert known for his ability to demystify complex TypeScript concepts.

Iterating over object keys in TypeScript can be a nightmare. See if you can solve an example of what I mean in the playground below:

Unless you know the tricks, it's not quite so simple. Here are all the solutions I know of.

Quick Explanation

  • Iterating using Object.keys doesn't work because Object.keys returns an array of strings, not a union of all the keys. This is by design and won't be changed.
ts
function printUser(user: User) {
Object.keys(user).forEach((key) => {
// Doesn't work!
console.log(user[key]);
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'.7053Element 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'.
});
}
  • Casting to keyof typeof in the right spot makes it work:
ts
const user = {
name: "Daniel",
age: 26,
};
 
const keys = Object.keys(user);
 
keys.forEach((key) => {
(parameter) key: string
console.log(user[key as keyof typeof user]);
});
  • A custom type predicate can also work by narrowing the type inline.
ts
function isKey<T extends object>(
x: T,
k: PropertyKey
): k is keyof T {
return k in x;
}
 
keys.forEach((key) => {
if (isKey(user, key)) {
console.log(user[key]);
(parameter) key: "name" | "age"
}
});

Longer Explanation

Object.keys

Here's the issue: using Object.keys doesn't seem to work as you expect. That's because it doesn't return the type you kind of need it to.

Instead of a type containing all the keys, it widens it to an array of strings.

ts
const user = {
name: "Daniel",
age: 26,
};
 
const keys = Object.keys(user);
const keys: string[]

This means you can't use the key to access the value on the object:

ts
const nameKey = keys[0];
const nameKey: string
 
user[nameKey];
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ name: string; age: number; }'. No index signature with a parameter of type 'string' was found on type '{ name: string; age: number; }'.7053Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ name: string; age: number; }'. No index signature with a parameter of type 'string' was found on type '{ name: string; age: number; }'.

There's a good reason that TypeScript returns an array of strings here. TypeScript object types are open-ended.

There are many situations where TS can't guarantee that the keys returned by Object.keys are actually on the object - so widening them to string is the only reasonable solution. Check out this issue for more details.

For...in loops

You'll also find this fails if you try to do a for...in loop. This is for the same reason - that key is inferred as a string, just like Object.keys.

ts
function printUser(user: User) {
for (const key in user) {
console.log(user[key]);
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'.7053Element 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'.
}
}

But for many cases, you'll feel confident that you know EXACTLY what shape that object is.

So, what do you do?

Solution 1: Cast to keyof typeof

The first option is casting the keys to a more specific type using keyof typeof.

In the example below, we're casting the result of Object.keys to an array containing those keys.

ts
const user = {
name: "Daniel",
age: 26,
};
 
const keys = Object.keys(user) as Array<keyof typeof user>;
 
keys.forEach((key) => {
(parameter) key: "name" | "age"
// No more error!
console.log(user[key]);
});

We could also do it when we index into the object.

Here, key is still typed as a string - but at the moment we index into the user we cast it to keyof typeof user.

ts
const keys = Object.keys(user);
 
keys.forEach((key) => {
(parameter) key: string
console.log(user[key as keyof typeof user]);
});

Using as in any form is usually unsafe, though - and this is no different.

ts
const user = {
name: "Daniel",
age: 26,
};
 
const nonExistentKey = "id" as keyof typeof user;
const nonExistentKey: "name" | "age"
 
// No error!
const value = user[nonExistentKey];

as is a rather powerful tool for this situation - as you can see, it lets us lie to TypeScript about the type of something.

Solution 2: Type Predicates

Let's look at some smarter, potentially safer solutions. How about a type predicate?

By using a isKey helper, we can check that the key is actually on the object before indexing into it.

We get TypeScript to infer properly by using the is syntax in the return type of isKey.

ts
function isKey<T extends object>(
x: T,
k: PropertyKey
): k is keyof T {
return k in x;
}
 
keys.forEach((key) => {
if (isKey(user, key)) {
console.log(user[key]);
(parameter) key: "name" | "age"
}
});

This awesome solution is taken from Stefan Baumgartner's great blog post on the topic.

Solution 3: Generic Functions

Let's look at a slightly stranger solution. Inside a generic function, using the in operator WILL narrow the type to the key.

I'm actually not sure why this works and the non-generic version doesn't.

ts
function printEachKey<T extends object>(obj: T) {
for (const key in obj) {
console.log(obj[key]);
const key: Extract<keyof T, string>
}
}
 
// Each key gets printed!
printEachKey({
name: "Daniel",
age: 26,
});

Solution 4: Wrapping Object.keys in a function

Another solution is to wrap Object.keys in a function that returns the casted type.

ts
const objectKeys = <T extends object>(obj: T) => {
return Object.keys(obj) as Array<keyof T>;
};
 
const keys = objectKeys({
name: "Daniel",
age: 26,
});
 
console.log(keys);
const keys: ("name" | "age")[]

This is perhaps the solution that's most prone to misuse - hiding the cast inside a function makes it more attractive and might lead to people using it without thinking.

Conclusion

My preferred solution? Usually, casting does the job perfectly well. It's simple and easy to understand - and is usually safe enough.

But if you like the look of the type predicate or generic solutions, go for it. The isKey function looks useful enough that I'll be stealing it for my next project.

Got any more questions? Found any more solutions? Let me know:

Matt's signature

Share this article with your friends

`any` Considered Harmful, Except For These Cases

Discover when it's appropriate to use TypeScript's any type despite its risks. Learn about legitimate cases where any is necessary.

Matt Pocock
Matt Pocock

No, TypeScript Types Don't Exist At Runtime

Learn why TypeScript's types don't exist at runtime. Discover how TypeScript compiles down to JavaScript and how it differs from other strongly-typed languages.

Matt Pocock
Matt Pocock

Deriving vs Decoupling: When NOT To Be A TypeScript Wizard

In this book teaser, we discuss deriving vs decoupling your types: when building relationships between your types or segregating them makes sense.

Matt Pocock
Matt Pocock

NoInfer: TypeScript 5.4's New Utility Type

Learn how TypeScript's new utility type, NoInfer, can improve inference behavior by controlling where types are inferred in generic functions.

Matt Pocock
Matt Pocock