Closed
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:
number
and${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'
and1
to be treated the same since they retrieve the identical property.+1
and0x0
to 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-literalstring
when initializing a const variable.
Additional information about the issue
Originally mentioned here.
This can surely be split into multiple issues.
Metadata
Metadata
Assignees
Type
Projects
Milestone
Relationships
Development
No branches or pull requests
Activity
numericStringType
#56878ahejlsberg 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:
string
index signature matches any type that is assignable tostring
ornumber
.number
index signature matches any type that is assignable tonumber
, the type`${number}`
, and any string literal type with a values
for which(+s).toString() === s
is true (i.e. numeric strings that "round trip").`${number}`
index signature matches any non-empty string literal type with a values
for which+s
produces a finite number.Aspects of these rules that are somewhat surprising:
number
index 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.number
index 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.number
index signature matches the template literal type`${number}`
, in effect making`${number}`
index signatures subsets ofnumber
index 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 withnumber
index 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.parse
would 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
/+n
on 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 numbertoString
s 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.