From Effective TypeScript: 62 Specific Ways to Improve Your TypeScript by Dan Vanderkam
It is an excellent book. Please purchase the book to support its author!
If you are the publisher and think this article should not be public, please write me an email to
trungk18 [at] gmail [dot] com
and I will make it private.
This code runs fine, and yet TypeScript flags an error in it. Why?
Inspect the obj
and k
symbols give a clue:
The type of k
is a 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
- let k: keyof typeof obj
fixes the issue:
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 typeof abc
). But in this case, TypeScript is right to complain. Here’s why:
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. 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:
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 error?
Object.entries lets you iterate over both simultaneously
interface ABC {
a: string
b: string
c: number
}
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)
}
//Print x y z
Hopefully, this doesn’t happen in a real 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 value in an object, use either:
keyof
declaration (let k: keyof T
)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 challenging to work with.
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.Object.entries
to iterate over the keys and values of any object.