-
Notifications
You must be signed in to change notification settings - Fork 12.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
revisit round-trip matching constraint for number literal inferencing #57404
Comments
I don’t think any of these are bugs because As for your addendum, that’s a different thing entirely (but is also intentional): #46124 (comment)
|
Thanks! I'm on the same page with you there about this being a consequence of the round-tripping. That's why I quoted Ron at the top the If it's more appropriate to convert this to a feature request, that's fine by me: I just want to know if there's any way to go about improving this (again, I have ideas, but I wanna make sure the problem domain is agreed on first). Seems like there should be some way to protect against most of the cases I brought up (hopefully 😄). Thanks also for the note re: the addendum! |
I don’t think It’s inconsistent but I’m not sure they have an appetite for doing anything to change it. |
haha, I totally know what you mean @jcalz re: appetite to improve type numbers. Actually, as a personal rule I try very hard to never submit "make numbers better" feature requests (as a matter of respect). In this case, I realized when you add it all up it's quite a mountain of strange behavior (until you realize what's really going on re: round-tripping). I think most people hit the "trailing zero" problem first, and it's the only one with a workaround (albeit a tad expensive, recursion-wise). My hope with this issue was to show all the places with the same root cause. When you look at it all from a distance.. it's a lot! And besides, I have the appetite to fix it now that it's thoroughly blocked me (hundreds of thousands of binary numbers, making recursive techniques a non-option). |
Nobody here is asking for my opinion but here it comes anyway: I think template literal types should only refer to what happens when you use a template literal string. So It is a very natural and reasonable thing to want to have some way to represent a string which could successfully be parsed as a number, but that's not what template literal strings do, at all; and pushing such semantics into template literal types feels like a category error to me. In my own personal version of TypeScript that lives only in my dreams, I would have a completely different set of tools for what you're trying to do that don't (ab)use template literals. Imagine an intrinsic type like But in the issue I referenced it was made clear that nobody but me wants it that way. Now this issue is requesting that such behavior be expanded to include inference. I think that's probably ultimately fine; I'd happily use such a feature if it existed (and it is a feature request, not a bug, this is working as intended). But it's hard to explain how or why this would have anything at all to do with template literals, leading to a weird mental model that does special crazy magic for numbers but not for, say, booleans. |
That would be fantastic! Last this came up the blocker was something to do with re: bug vs feature request. sure: that's fine by me :) -> I guess all I wanted to know first is something like:
After all, not every observable behavior of the number inferring has been considered by-design, for example whitespace infers to number and is accepted as a bug. To me, some of the things in this PR are not far afield from that one. |
@jcalz FWIW, I agree with you - especially given @ahejlsberg’s stated insistence that template type inference not “turn into another regex engine”. It would make perfect sense to me if Of course, I’m also a pragmatist who recognizes that what I want doesn’t really square with the way template type inference is used by TS coders today; as such, I consider this genie to be already out of the bottle, and have to reluctantly agree with the implied feature request: if it’s indeed intentional that |
I don't know how we'd ever square this circle in a way that people found acceptable. Probably 99.9% of embedded
A crash is when tsc exits abnormally due to an exception. Untasteful behavior is not a crash 😉 |
...well yes, that's the point of the issue - |
@RyanCavanaugh ohhhhh as in a runtime crash! sorry about that! I stared at the options on the form for a long time
and I was thinking about it in terms of "a type that parses can suddenly stop parsing and return never" but now I see that was a pretty near-sighted of me, haha. I almost forgot that <sarcasm>some people use TypeScript for more than just the type system</sarcasm>! heh. sorry! It seems like that's a clear indication it should be a feature request so I updated it as such (as best I could, hopefully I didn't screw something up). re:
I realized that I didn't say it above anywhere but I just wanted to clarify that I find the currently implementation completely acceptable. Anyone who says that TypeScript hasn't "gone far enough" with this stuff is being silly. ✨ TypeScript is wonderful ✨ even if you can find ways (like So on that note @rbuckton since you're assigned to this I wanted to say the intentions for making this issue were:
|
Hi!
Example: |
@dimitropoulos You forgot the ".<digits>" format in your table, that doesn't work either. |
While there may be some odd quirks to review for some cases, the general principle is that you can only use |
As to the specific use cases mentioned:
The canonical string representations of
This is specific to how JavaScript formats IEEE floats for a radix of 10. Per Step 6 of Number::toString. Once you have passed a specific order of magnitude in either direction, the canonical string representation uses the scientific notation form (Steps 7+ of the algorithm). This range is from -5 to 21 -
The canonical string representation of a given
Numeric separators are not preserved in an actual
The canonical string representation for a given BigInt never includes the I don't believe any of these should be supported by |
Thanks @rbuckton! That's the answer I was looking for. I really appreciate you taking a closer look. I just wanted clarification if all of these are intended, and it seems like they are so.. that's that! I continue to be eternally grateful for the powerhouse of engineering known as TypeScript. I'll be fine without this little wrinkle (of JavaScript itself, ultimately) being ironed out.
Totally agree. I'm sure anyone that tries to make the case for this in the future will refence this issue or the cases I mentioned, but I won't be the one to file it! haha. For the 100th time. Hats off to the TypeScript team for what's already possible. Anyone who reads this issue and somehow draws the conclusion that TypeScript isn't good enough.... I'd suggest you reconsider. |
The round-trip constraint for numeric literals ensures consistency in TypeScript, as noted in the discussion. Using TypeScript types to parse number formats can address many edge cases. Here’s an example approach: https://tsplay.dev/weAbgw |
🔎 Search Terms
infer number, extends number, extends bigint, binary number, number representation, number notation, exponential notation, binary notation, hex numbers, hexadecimal numbers, hexadecimal notation, literal numbers, number literals
✅ Viability Checklist
⭐ Suggestion
Back in #48094, constrained "infer" types in template literals were set to be limited by a "round trip" constraint. Meaning: numeric inference of string literals would only be allowed for literals that remain the same going from
string
tonumber
and then back tostring
again. E.g.:"123"
satisfies the constraint because"123"
->123
->"123"
"0x10"
isn't allowed because"0x10"
->16
->"16"
That makes sense. I can certainly understand the tradeoff of preferring type system performance and simplicity over the nuance edge case of number-to-string conversion. These type-system arithmetics weren't a common use case for end users at the time.
However, since #48094, there've been quite a few use cases that have popped up.
Some of them even impact real-world libraries* that have been inconvenienced by not being able to infer number literals that don't satisfy the round-trip constraint.
Let's consider a common
ToNumber
utility type and less-common variantToBigInt
, defined roughly as:The following table has:
number
number
Fractional number representations ending in zero are probably the one that people hit the most.
This is one that people try to use recursion to fix. For example, see @anuraghazra's attempt at fixing this problem (link). Since you pay one recursion tax per digit of the number this is probably ok since the recursion max is 100.
1e-6 Boundary
number
number
For small numbers, TypeScript switches its underlying notation somewhat arbitrarily at the 1e-6 boundary.
This causes a "flip" where you can infer numbers between the 0 and 1e-6 boundary, but as soon as your number gets smaller than that, the inferencing breaks if you're not using the same notation.
1e20 Boundary
number
number
number
number
This one is sort of an inverse of the above (just for large numbers) but with an added footgun: if you don't have the + sign once you get into the e-notation range, then it will also not work because TypeScript always includes the + in this range.
number
number
number
Binary, Hexadecimal, and Octal number notations don't work. Common approaches for binary require lots of recursion if you have a scenario where you need to convert a binary number to decimal.
Hex numbers perhaps with even more use cases. A lot of the use-cases that are listed in #54925 also apply here (e.g. RGB values, reading bytes, etc.).
never
never
never
never
never
2n
as a literal works butToBigInt<"2n">
results innever
andToBigInt<"2">
is the way to get2n
. This is different from the above because in this situation the string representation and the number representation do match but it only works if the input is not a bigint. This seems to break the round-trip rule, because in this case if the input is2n
and the output is2n
then you'd think they'd match.also: a quirky consequence regarding `-0n` (don't laugh)
I also noticed that there's a (presumably unintended) behavioral mismatch regarding
-0n
. As far as I can tell, this is the only situation where you can get abigint
out "the other side".There is no negative-zero BigInt as there are no negative zeros in integers. -0.0 is an IEEE floating-point concept that only appears in the JavaScript Number type (source). Yet, TypeScript allows it. I think that's sorta fine because, actually the BigInt constructor also allows it, and that's presumably what this code courses through anyway.
⏯ Playground Link
Playground Link
thanks to @JoshuaKGoldberg for suggestions on how to clean up this issue's formatting
The text was updated successfully, but these errors were encountered: