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:

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 »