There was some work done in microsoft/TypeScript#26797 on allowing index signatures with arbitrary property-key types. It's unfortunately stalled since it's not clear how to deal with a mismatch between mapped type and index signature behavior. Index signatures, as currently implemented, have different and more unsound behavior than mapped types. You noticed this by assuming that you could assign {}
to a mapped type whose keys are ResourceConstant
, and by being told no because the properties are missing. Index signatures currently don't care if properties are missing, which is convenient but unsafe. Some work would need to be done to make mapped types and index signatures more compatible in order to proceed. Maybe the upcoming pedantic index signatures --noUncheckedIndexedAccess
feature will unblock this? For now, you have to use mapped types.
Converting from an index signature to a mapped type is easy enough syntactically: you can change from {[k: SomeKeyType]: SomeValueType}
to {[K in SomeKeyType]: SomeValueType}
. This is the same as the Record<SomeKeyType, SomeValueType>
utility type. And yes, if you want to leave out some keys, you could use Partial<>
.
Some people like Partial<Record<K, T>>
because it is a more "English"-like description of what you're doing. Still, if you find it horrible , you can reduce your horror by writing the mapped type yourself:
let x: { [K in ResourceConstant]?: number } = {};
Now for the for..in
loop stuff.
It would definitely be nice if you were allowed to annotate the iterating variable declaration inside for..in
or for..of
loops. Currently it can't be done at all (see microsoft/TypeScript#3500 for more info.)
But letting you annotate res
as ResourceConstant
would be a problem.
The big sticking point here is that object types in TypeScript are open, or extendible, and not closed, or exact. An object type like {a: string, b: number}
means "this object has a string
-valued property at key a
and a number
-values property at key b
", but it does not mean "and there are no other properties". Extra properties are not prohibited. So a value like {a: "", b: 0, c: true}
is assignable. (This is complicated by the fact that if you try to assign a fresh object literal with extra properties to a variable, the compiler does perform excess property checking But these checks are easy to circumvent; see the documentation link for more info).
Let's therefore imagine that we could write for (let res: ResourceConstant in x)
without warning:
function acceptX(x: { [K in ResourceConstant]?: number }) {
for (res: ResourceConstant in x) { // imagine this worked
console.log((x[res] || 0).toFixed(2));
}
}
Then nothing stops me from doing this:
const y = { A: 1, B: 2, D: "four" };
acceptX(y); // no compiler warning, but this explodes at runtime
// 1.00, 2.00, and then EXPLOSION!
Oops. I assumed that for (let res in x)
would only iterate over some of A
, B
, and C
. But at runtime a D
got in there and messed everything up.
That's why they don't let you do that. In the absence of exact types, it's just not safe to iterate over whatever keys happen to be in an object of a "known" type. So, you could either be safe like this:
for (let res in x) {
if (res === "A" || res === "B" || res === "C") {
console.log((x[res] || 0).toFixed(2)); // no error now
}
}
or this:
// THIS IS THE RECOMMENDED SOLUTION HERE
for (let res of ["A", "B", "C"] as const) {
console.log((x[res] || 0).toFixed(2));
}
OR, you could be unsafe and use a type assertion or other workaround. The obvious assertion workaround is to create a new variable like this:
for (let res in x) {
const res2 = res as ResourceConstant;
console.log((x[res2] || 0).toFixed(2));
}
Or assert every time you mention res
,
for (let res in x) {
console.log((x[res as ResourceConstant] || 0).toFixed(2));
}
but if that's too horrible then you can use the following workaround (from this comment):
let res: ResourceConstant; // declare variable in outer scope
for (res in x) { // res in here uses same scope
console.log((x[res] || 0).toFixed(2));
}
Here I've declared res
in the outer scope as the type I wanted, and then just use it inside the for..in
loop. This apparently works with no error. It's still unsafe, but maybe you find it more palatable than the other alternatives.
I think your frustration with TypeScript is understandable... but I hope I've conveyed that the walls you're hitting are there for reasons. In the case of mapped types and index signatures, the wall happens to connect two usable hallways but the architects don't yet know how to cut a door in it without having one side or the other collapse. In the case of annotating for..in
loop indexers it's to stop people from falling into the open pit on the other side of that wall.
Playground link to code