Repository navigation
Wildly unpredictable behavior for resolving number-type index expressionsΒ #57296
Copy link
Copy link
Closed
Labels
Working as IntendedThe behavior described is the intended behavior; this is not a bugThe behavior described is the intended behavior; this is not a bug
Description
π Search Terms
numeric index property access
π Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about index signatures
β― Playground Link
π» Code
// @target:ES2022
declare const o : {
[x:string]:'string'|'number'|'numstring',
[x:number]:'number'|'numstring',
[x:`${number}`]:'numstring'
}
const p1 = o[1]
// ^?
const p2 = o['1']
// ^?
const p3 = o['one']
// ^?
const p4 = o['0x0']
// ^?
const p5 = o['Infinity']
// ^?
const p6 = o[`${300n}`]
// ^?
const p7 = o['123_456']
// ^?
const p8 = o[123_456]
// ^?
const p9 = o['-1']
// ^?
const p10 = o['+1']
// ^?
const p11 = o[`${[6]}`]
// ^?π Actual behavior
The inferred types of these index expressions are very unpredictable.
The indexes '1','0x0',`${300n}`, -1, +1 are inferred as `${number}`, and NOT as 'number'.
The indexes 1, 'Infinity', 123_456, are inferred as number
The indexes 'one', '123_456', '${[6]}' are inferred as string (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')- Possibly
${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.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
Working as IntendedThe behavior described is the intended behavior; this is not a bugThe behavior described is the intended behavior; this is not a bug
Activity
ahejlsberg commentedon Feb 22, 2024
I wouldn't say our behavior is wildly unpredictable, but it is perhaps a bit surprising in some cases. These are the rules:
stringindex signature matches any type that is assignable tostringornumber.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").`${number}`index signature matches any non-empty string literal type with a valuesfor which+sproduces a finite number.Aspects of these rules that are somewhat surprising:
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.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.`${number}`index signature doesn't match"Infinity","-Infinity", or"NaN"because we require the resulting number to be finite. This doesn't seem unreasonable.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.
rotu commentedon 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:" 1 \r \n \t "is assignable to${number}even though it probably is not what's intended.${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.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.RyanCavanaugh commentedon Feb 22, 2024
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).rotu commentedon Feb 22, 2024
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!ahejlsberg commentedon Feb 23, 2024
Regarding
It should say "non-empty string literal type". I've modified the original comment to reflect this.