Repository navigation
Wildly unpredictable behavior for resolving number-type index expressionsย #57296
Description
Activity
- addedNeeds InvestigationThis issue needs a team member to investigate its status.This issue needs a team member to investigate its status.
on Feb 5, 2024 I wouldn't say our behavior is wildly unpredictable, but it is perhaps a bit surprising in some cases. These are the rules:
- A
stringindex signature matches any type that is assignable tostringornumber. - A
numberindex signature matches any type that is assignable tonumber, the type`${number}`, and any string literal type with a valuesfor which(+s).toString() === sis true (i.e. numeric strings that "round trip"). - A
`${number}`index signature matches any non-empty string literal type with a valuesfor which+sproduces a finite number. - When multiple index signatures are applicable, their element types are intersected.
Aspects of these rules that are somewhat surprising:
- A
numberindex signature requires matching numeric strings to "round-trip", but a`${number}`index signature does not. The round-trip requirement is intentional to reflect JavaScript numeric indexing semantics. - A
numberindex signature matches the string literal types"Infinity","-Infinity", and"NaN"(just like it matches the string literal types"0"and"123"). Again, this is intentional to reflect JavaScript's numeric indexing semantics. - A
`${number}`index signature doesn't match"Infinity","-Infinity", or"NaN"because we require the resulting number to be finite. This doesn't seem unreasonable. - A
numberindex signature matches the template literal type`${number}`, in effect making`${number}`index signatures subsets ofnumberindex signatures. This is debatable since`${number}`doesn't come with the round-trip requirement, but it isn't entirely unreasonable (e.g. it makes types with`${number}`index signatures satisfy types withnumberindex signatures).
Overall, it's not clear to me that anything needs to change here.
Reacted by Ryan Cavanaugh and Dan Rose- A
- addedWorking as IntendedThe behavior described is the intended behavior; this is not a bugThe behavior described is the intended behavior; this is not a bugand removedNeeds InvestigationThis issue needs a team member to investigate its status.This issue needs a team member to investigate its status.
on Feb 22, 2024 Wow. That makes so much more sense when you explain it like that!
The round-trip issue isn't a big problem, but did hinder me trying to grok the actual behavior.
The only thing I think is an important bug is that
${number}has the wrong semantics:- It's surprising.
" 1 \r \n \t "is assignable to${number}even though it probably is not what's intended. - It's backwards by analogy with other types.
${boolean},${null},${undefined}all mean "the string values which boolean/null/undefined may become when interpolated into a string", not "values that may coerce to boolean/null/undefined". These other types also forbid leading/trailing whitespace.
A
${number}index signature matches any string literal type with a value s for which +s produces a finite number.The implementation doesn't quite match the description you gave.
""is not assignable to${number}even though+""evaluates to the number0. I'm guessing you meant to say the strings thatJSON.parsewould turn into numbers. This would also explain why leading/trailing whitespace is tolerated.- It's surprising.
RyanCavanaugh commented
on Feb 22, 2024 MemberMore actionsIt's surprising. " 1 \r \n \t " is assignable to ${number} even though it probably is not what's intended.
I think most people's mental model is that if they have
${number}in a template string, it means they're gonna callparseInt/parseFloat/+non it, all of which happily eat whitespace.The reverse interpretation - that
${number}only accept strings which can come from numbers - has undesirable consequences like forbidding "2.0" (since no numbertoStrings to it).Reacted by Dan RoseI think most people's mental model is that if they have
${number}in a template string, it means they're gonna callparseInt/parseFloat/+non it, all of which happily eat whitespace.The reverse interpretation - that
${number}only accept strings which can come from numbers - has undesirable consequences like forbidding "2.0" (since no numbertoStrings to it).That doesn't match my mental model, but perhaps I'm in the minority. I would expect that if something is number-parseable, you have already parsed it!
Anyway, thank you for explaining. This is clearly at least somewhat by design and I think my issues are covered by #46109 and #57404. Though the docs definitely could clarify what
${number}means!Reacted by Ryan CavanaughRegarding
A ${number} index signature matches any string literal type with a value s for which +s produces a finite number.
It should say "non-empty string literal type". I've modified the original comment to reflect this.
Reacted by Dan Rose- locked as resolved and limited conversation to collaborators
on Oct 22, 2025
๐ Search Terms
numeric index property access
๐ Version & Regression Information
โฏ Playground Link
https://www.typescriptlang.org/dev/bug-workbench/?target=9#code/PTAEAEBcEMCcHMCmkBcBRAygJgAxawFAAmiAxgDZyKikD2AdgM6Si2gqgDeBovoA2gA8UzWAEt68ALooA5KInxZAH1n0ArgFsARolgq1WhZNkAaHnyEoNOvTMO39qm8aXm+A4QAMAJJxu6sAC+XvYukOImBEEEdEwsAA4AjKAAvKz8SVIEIB4AegD8sQzMoAlYaRmySbLZuXyFxfFlAMyVtPyyDIi1OWANRXGlCQAs7Z04gji99byNQ4kArOOyAJL0AGYSYpAAnjP9c4MliQBs476cLTg49CF1h6DzJ2UA7CtJWC0A+iOLpwd8sdmgkABzjT4-P6nB5AprDACcKwAtDVYQN4YkkjgVgBqNF9OELMpJFLpDqXfgw+6EgZAA
๐ป Code
๐ Actual behavior
The inferred types of these index expressions are very unpredictable.
The indexes
'1','0x0',`${300n}`,-1,+1are inferred as`${number}`, and NOT as 'number'.The indexes
1,'Infinity',123_456, are inferred asnumberThe indexes
'one','123_456','${[6]}'are inferred asstring(these are understandable and not a problem)๐ Expected behavior
I expect:
numberand${number}to catch the same cases, since (I believe) they express the same intent as object keys. Likely they should be considered a duplicate index signature in the type declaration.'1'and1to be treated the same since they retrieve the identical property.+1and0x0to be inferred as non-numeric string, since (I think) the${number}is supposed to be "the strings that a number may coerce to", not "the strings which are valid numeric literals". (though it's curious that strings containing binary, octal, and hex literals are accepted by the template string but NOT digit separators'123_456')${300n}to be inferred as a non-numeric string. It's odd that it is treated as the string literal"300"in the index but a non-literalstringwhen initializing a const variable.Additional information about the issue
Originally mentioned here.
This can surely be split into multiple issues.