Here's the code (Playground Link):
interface XY {x: number, y: number}
function mcve(current: XY | undefined, pointers: Record<string, XY>): void {
if(!current) { throw new Error(); }
while(true) {
let key = current.x + ',' + current.y;
current = pointers[key];
}
}
The code in this example isn't meant to do anything useful; I removed everything that wasn't necessary to demonstrate the issue. Typescript reports the following error at compile-time, on the line where the variable key
is declared:
'key' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
As far as I can tell, at the start of each iteration of the loop, Typescript knows that current
is narrowed to type XY
, and that current.x
and current.y
are each of type number
, so it should be straightforward to determine that the expression current.x + ',' + current.y
is of type string
, and infer that key
is of type string
. The way I see it, a string concatenation should obviously be of type string
. However, Typescript does not do this.
My question is, why isn't Typescript able to infer that key
is of type string
?
In investigating the issue, I've discovered several changes to the code which cause the error message to go away, but I'm not able to understand why these changes should matter to Typescript in this code:
- Giving
key
an explicit type annotation: string
, which is what I've done in my real code, but it doesn't help me understand why this can't be inferred. - Commenting out the line
current = pointers[key]
. In this casekey
is correctly inferred asstring
, and I don't understand why the subsequent assignment tocurrent
should make this harder to infer. - Changing the
current
parameter's type fromXY | undefined
toXY
; I don't understand why this should matter, sincecurrent
does have typeXY
at the start of the loop by control-flow type narrowing. (If it didn't then I'd expect an error like "current
could beundefined
" instead of the actual error message.) - Replacing
current.x
andcurrent.y
with some other expressions of typenumber
. I don't understand why this should matter, sincecurrent.x
andcurrent.y
do have typenumber
in that expression. - Replacing
pointers
with a function of type(s: string) => XY
and replacing the index access with a function call. I don't understand why this should matter, because an index access of aRecord<string, XY>
seems like it should be equivalent to a function call of type(s: string) => XY
, given that Typescript does assume the index will be present in the record.