Item 54: Know How to Iterate Over Objects

Iterating over the keys and values in an object is a common operation that's surprisingly hard to write without type assertions in TypeScript. The difficulty results from a combination of the quirks of JavaScript objects and duck typing. Reading through this item again, I'd add that this is all a good reason to consider using an ES6 Map instead of an object to store key/value pairs!

This code runs fine, and yet TypeScript flags an error in it. Why?

const obj = {
one: 'uno',
two: 'dos',
three: 'tres',
};
for (const k in obj) {
const v = obj[k];
// ~~~~~~ Element implicitly has an 'any' type
// because type ... has no index signature
}

Inspecting the obj and k symbols gives a clue:

const obj = { /* ... */ };
// const obj: {
// one: string;
// two: string;
// three: string;
// }
for (const k in obj) { // const k: string
// ...
}

The type of k is string, but you're trying to index into an object whose type only has three specific keys: 'one', 'two', and 'three'. There are strings other than these three, so this has to fail.

Plugging in a narrower type declaration for k fixes the issue:

let k: keyof typeof obj;  // Type is "one" | "two" | "three"
for (k in obj) {
const v = obj[k]; // OK
}

So the real question is: why is the type of k in the first example inferred as string rather than "one" | "two" | "three"?

To understand, let's look at a slightly different example involving an interface and a function:

interface ABC {
a: string;
b: string;
c: number;
}

function foo(abc: ABC) {
for (const k in abc) { // const k: string
const v = abc[k];
// ~~~~~~ Element implicitly has an 'any' type
// because type 'ABC' has no index signature
}
}

It's the same error as before. And you can "fix" it using the same sort of declaration (let k: keyof ABC). But in this case TypeScript is right to complain. Here's why:

const x = {a: 'a', b: 'b', c: 2, d: new Date()};
foo(x); // OK

The function foo can be called with any value assignable to ABC, not just a value with "a," "b," and "c" properties. It's entirely possible that the value will have other properties, too (see Item 4: Get Comfortable with Structural Typing). To allow for this, TypeScript gives k the only type it can be confident of, namely, string.

Using the keyof declaration would have another downside here:

function foo(abc: ABC) {
let k: keyof ABC;
for (k in abc) { // let k: "a" | "b" | "c"
const v = abc[k]; // Type is string | number
}
}

If "a" | "b" | "c" is too narrow for k, then string | number is certainly too narrow for v. In the preceding example one of the values is a Date, but it could be anything. The types here give a false sense of certainty that could lead to chaos at runtime.

So what if you just want to iterate over the object's keys and values without type errors? Object.entries lets you iterate over both simultaneously:

function foo(abc: ABC) {
for (const [k, v] of Object.entries(abc)) {
k // Type is string
v // Type is any
}
}

While these types may be hard to work with, they are at least honest!

You should also be aware of the possibility of prototype pollution. Even in the case of an object literal that you define, for-in can produce additional keys:

> Object.prototype.z = 3; // Please don't do this!
> const obj = {x: 1, y: 2};
> for (const k in obj) { console.log(k); }
x
y
z

Hopefully this doesn't happen in a nonadversarial environment (you should never add enumerable properties to Object.prototype), but it is another reason that for-in produces string keys even for object literals.

If you want to iterate over the keys and values in an object, use either a keyof declaration (let k: keyof T) or Object.entries. The former is appropriate for constants or other situations where you know that the object won't have additional keys and you want precise types. The latter is more generally appropriate, though the key and value types are more difficult to work with.

Things to Remember

  • Use let k: keyof T and a for-in loop to iterate objects when you know exactly what the keys will be. Be aware that any objects your function receives as parameters might have additional keys.
  • Use Object.entries to iterate over the keys and values of any object.
Like this post? Consider subscribing to my newsletter, the RSS feed, or following me on Twitter.
comments powered by Disqus
Effective TypeScript Book Cover

Effective TypeScript shows you not just how to use TypeScript but how to use it well. The book's 62 items help you build mental models of how TypeScript and its ecosystem work, make you aware of pitfalls and traps to avoid, and guide you toward using TypeScript’s many capabilities in the most effective ways possible. Regardless of your level of TypeScript experience, you can learn something from this book.

After reading Effective TypeScript, your relationship with the type system will be the most productive it's ever been! Learn more »