I was trying to wrap my head around what a number index signature is and ran into something that felt strange. This may be WAI but it is certainly surprising.
TypeScript Version: 3.5.1
Search Terms:
Code
const xs: {[k: number]: number} = [1, 2, 3]; // acts same w/o explicit index signature
const x0 = xs[0]; // ok, type is number
const x1 = xs['1'];
// ~~~~ Element implicitly has an 'any' type
// because index expression is not of type 'number'.
declare let key: string;
const x = xs[key];
// ~~~ Element implicitly has an 'any' type
// because index expression is not of type 'number'.
for (const k of Object.keys(xs)) {
const x = xs[k]; // also an error
}
for (const k in xs) {
k; // type is string
const x = xs[k]; // ok, type is number (!)
}
Expected behavior:
I would have expected the access in the for loop to fail with a type error. string is not the index type for xs, number is.
Actual behavior:
The code in the last loop type checks.
Either string should be allowed as an index type or it should not. k's type in the last loop is shown as string but it seems that it must be something else since this is permitted.
This code works, of course: at runtime k's type _is_ string and x's type in the loop _is_ number. But so does the Object.keys variant, as well as the direct access (x['1']).
for-in is a terrible way to iterate over an array. If you don't care about the index you should use for-of. If you do, you can use forEach or for(;;) to get a numeric index.
Playground Link: link
Related Issues:
Thanks @jcalz, this comment in particular clarifies that there's a "huge" amount of code out there that uses this access pattern, so breaking it all would be jarring.
Fine to close this as WAI. What surprises me is just that I'm not aware of any other situations in TypeScript where there's hidden type information like this (k's type is shown as string but it's actually a string with one special ability).
I presume the type of k is actually keyof typeof xs. AFAICT keyof is not referentially transparent: as a type it seems to encode extra information about which object it came from which is what makes e.g. homomorphic mapped types work (i.e. it's how TS knows the difference between [K in KeyUnion] vs. [K in keyof T]).
@fatcerberus the type is string, not keyof xs. There's been much discussion of why, but the gist is that, because of structural typing, a value may have more properties at runtime than are declared on its static type. And these will be enumerated by for-in.
No, I know how for..in works, I was speculating on why k behaves differently from an explicit string:
k's type in the last loop is shown as string but it seems that it must be something else since this is permitted.
and my conclusion was that the compiler must be treating it internally as a keyof type (or something like that) despite the intellisense claiming it鈥檚 a regular string.
It's treating it as the elusive "NumericString", a type which turns into string when you look at it, but when you look away briefly it turns into a numeric index. In my opinion it would be a great thing to surface this as a real and observable subtype of string which matches any string value s for which s === ""+(+s).
So it鈥檚 basically Schr枚dinger鈥榮 String. Gotta love all the hidden magic in the TS type system. :smile:
Most helpful comment
It's treating it as the elusive "
NumericString", a type which turns intostringwhen you look at it, but when you look away briefly it turns into a numeric index. In my opinion it would be a great thing to surface this as a real and observable subtype ofstringwhich matches any string valuesfor whichs === ""+(+s).