TL;DR: Use as
or write a redundant array of keys.
The TypeScript standard library's typing for Object.entries(obj)
, like its typing for Object.keys(obj)
, returns keys of type string
, and not keyof typeof obj
. This is frustrating to many people, but it works that way for an important reason: object types in TypeScript are open or extendible, and not closed or exact.
Consider the following code:
interface Foo {
x: string;
}
interface Bar extends Foo {
y: number;
}
const b: Bar = { x: "", y: 1 };
const f: Foo = b; // okay
f.y; // error, y is not known to exist on Foo
console.log(Object.entries(f)); // [["x",""],["y",1]];
An object of type Foo
is known to have a string
-valued x
property, but it is not known to lack other properties. Since Bar extends Foo
, it means every Bar
is a valid Foo
, so there could be some Foo
objects with a number
-valued y
property. Or really, any other unspecified property. The only property of a Foo
object that we know anything for certain about is x
. So Object.keys(f)
needs to return an array of at least "x"
, and maybe all kinds of other things. So that becomes just string
.
There's not much to be done here, then. The compiler doesn't have a representation for types that it knows have no extra properties. Its excess property checking functionality only works for object literals that have not yet been assigned anywhere. Once you have assigned it to the assets
variable, the compiler treats it like any other object-typed variable. It has mapTileset
and characterTileset
keys, and maybe all sorts of other keys.
If you want to tell the compiler "no, it's just those keys", then you can use a type assertion; either the way you have done, or at the Object.entries()
call:
(Object.entries(assets) as [assetKey, string][]).forEach(([key, assetSrc]) => {
const asset = new Image();
asset.onload = this.onLoad;
asset.src = assetSrc;
assetMap[key] = asset;
});
They're both the same thing, though: you telling the compiler what it can't figure out for itself.
The other way to proceed is to make up your own array of known keys, like this:
(["mapTileset", "characterTileset"] as const).forEach(key => {
const asset = new Image();
asset.onload = this.onLoad;
asset.src = assets[key];
assetMap[key] = asset;
});
This doesn't use a type assertion at all, really. The as const
there is called a const
assertion and asks the compiler to infer the array not as string[]
, but as a readonly pair of string literals. The code works without a problem and is arguably safer than your other code: if assets
somehow does get other properties, you won't accidentally use them... but you pay for this safety by having to write a redundant array.
Usually I just use a type assertion, and move on.
Okay, hope that helps; good luck!
Playground link