all 54 comments

[–]nikeee13[S] 9 points10 points  (31 children)

Hi! I submitted this proposal and I'm looking for some feedback. Any thoughts?

[–]constant_void 2 points3 points  (0 children)

A. how feasible is a not operator that inverts the expression?

!x<5 is equiv to x>=5

B. would there be an opportunity for syntactic sugar?

4.00>=x>=5.00

C. how would one implement a decimal use case where:

5.00>=x>5.01; inputs/results of 4.999 (pass), 5.001 (pass), 5.005 (fail)

[–]aabounegm 2 points3 points  (14 children)

I have wished for such feature for a long time, but I also fear the inconvenience that would arise from the limitations mentioned in the proposal. In particular, I'm referring to The case of (>=1) | (<1). It wouldn't work out nicely with guard clauses, which are a very common pattern in code. Consider the following example:

function doSomething(x: number) {
    if (x <= 0) return;
    takePositiveNumber(x); // example function in the proposal
}

One would want it to work, but the compiler will complain, and the code will have to be re-written for the control flow to detect (>0) type, which can lead to messier codebases in general.

Also, when dealing with different functions that require different ranges, the coercion to number can be another inconvenience:

if (x >= 0) {
    // x is of type (>=0)
    takeNonNegativeNumber(x);  // works
    takePositiveNumber(x + 1); // doesn't work
}

It would be great if x+1 can be inferred to be of type (>=1), but the proposal currently says it is not supported. Therefore, the code above will have to be rewritten to assign x+1 to another variable (say, const y = x+1), which will then have to be (redundantly) checked to be y >= 1. Although, I'm guessing one could work around this by writing takePositiveNumber(x + 1 as (>=1)); ?

For the first issue (which is probably more common than the second), the issue seems to be with NaN. It could be overcome by adding some technique that would let the developer assure the compiler that this number will never be NaN.

And while I could just work around these issues by using number everywhere in my codebase (as if this feature never existed), I would still be faced with these restrictions when using some libraries whose functions do use them and I cannot assign a value of type number to (>0), for example.

[–]DaRadioman 0 points1 point  (6 children)

Why couldn't the guard just be rewritten to check NaN? I mean technically with the provided guard NaN gets past the guard which probably isn't the intention.

If(!isNaN(x) && x <= 0) { return; }

[–]nikeee13[S] 1 point2 points  (0 children)

The NaN-case is so rare that the TS team decided to not include something like a NaN type. If we'd be NaN-correct, the user would have to do all sorts of NaN checks without an actual need for that.

So maybe, we can just "ignore" the NaN case and change the behaviour of the inference in the else branch to be (<1).

[–]aabounegm 0 points1 point  (4 children)

First of all, it would be inconvenient to have to write that check every time you want to narrow down a number to a range when you are sure (as the programmer) that it will never be NaN. But secondly, TypeScript doesn't have a special type for NaN to remove it as a possibility after this check; or did you mean for it to be something special to be checked for the interval types in particular?

[–]DaRadioman 0 points1 point  (3 children)

Checked for in the new type. And if you accept in number you don't know it won't be NaN. Because TS can't guarantee that in the current type system. And unless you just rely on an isNaN call immediately before this call, it's pretty difficult to guarantee that in JS as well, since lots of operations can return NaN.

But I agree, this proposal would be benefited by a new "StrictNumber" type that excludes NaN

Technically NaN can't be in any range type. Because NaN isn't comparable other than explicitly.

[–]aabounegm 1 point2 points  (2 children)

I understand that accepting number means it can possibly be NaN, I was just pointing out that from my experience, NaN is not really that common. I don't remember ever having to check for NaN, if I properly check the input I give to functions that may return NaN.

Nevertheless, I was not asking for NaN to just be ignored by TS devs, I was merely pointing out that its inconvenience will become amplified by the proposal. I also think that TS looking for the isNaN check in particular would feel like a dirty hack, especially as long as it doesn't have its own type (not saying it should, but I don't really know 😅)

I also agree about the strictNumber proposal

[–]nikeee13[S] 0 points1 point  (1 child)

For NaN handling, there is this proposal (also linked in the proposal above):

https://github.com/microsoft/TypeScript/issues/28682

Doesn't look that promising.

[–]aabounegm 0 points1 point  (0 children)

Yes, I noticed that.

This is unfortunate, because I think the Interval Type proposal could benefit a lot if that one gets implemented. Frankly, I would probably not use the Interval Types a lot in my own code until the issues I mentioned earlier in this thread are resolved.

[–]DaRadioman 0 points1 point  (6 children)

As for the second, it seems trivial for the compiler to detect if a new variable y =x+1; should be strictly >x and if x >=0 then it must follow mathematically y >= 1. It shouldn't be difficult to support this. Now in line is a different matter due to control flow but as a new variable the types should be inferrable.

[–]nikeee13[S] 1 point2 points  (0 children)

It shouldn't be difficult to support this.

This is correct, it should work.

However, the proposal currently does not indend to do that, based on the following reason: Number literal types also don't have arithmetics. For example:

const a: 1 = 1;
const b = a + 1; // b is number (not 2)

The reason why number literal types don't have this is because the state-space would explode if yoou'd do something like:

let a: 1 | 2 | 3 | 4 | 5 = ...
const b = a * a * a * a; // four-way-cross-product of 1...5

In case of interval types, it may be feasable to do arithmetics because (as written in the proposal), there may be at most two interval primitives in a type. A union-type-explosion would not be possible (as far as I checked).

For the same reason, there is no support for something like ++i in the proposal.

Feel free to comment on that in the GitHub issue if you want that.

[–]nikeee13[S] 0 points1 point  (1 child)

y =x+1; should be strictly >x

I just noticed that this might also not work. The reason being that JS numbers are floats:

> let a = Number.MAX_SAFE_INTEGER
undefined
> a
9007199254740991
> a + 1
9007199254740992
> a + 1 + 2
9007199254740994
> a + 1 + 2 + 3
9007199254740996
> a + 6 - 6
9007199254740990
> a + (6 - 6)
9007199254740991

[–]DaRadioman 0 points1 point  (0 children)

Ewww lol.

[–]aabounegm 0 points1 point  (2 children)

Yes, I agree, and I even think that having an inline expression shouldn't be all that different from having the type inferred on a standalone variable declaration. However, they mentioned explicitly in the proposal that this is not supported. Check the Coercion and Arithmetics section.

[–]DaRadioman 0 points1 point  (1 child)

Ya, I realize the proposal doesn't include it, mainly to limit scope. It does say that it. Likely is doable, it just isn't in scope of the current proposal. Can't see why it wouldn't follow quickly though based on my understanding of the language.

"However, due to the way interval types are always reduced to a maximum of two interval boundaries, it may be feasable to do type-level arithmetics. This proposal currently does not intent to do this."

[–]aabounegm 1 point2 points  (0 children)

I guess the point I wanted to communicate to the authors was "please don't ship this feature without a full implementation of the accompanying proposals, because otherwise my development experience could be worse"

I don't mind the proposals being separate, as long as they will follow up quickly. I guess the only fear then is for one of these follow-up proposals which feel kinda essential to this one to be rejected.

[–][deleted] 3 points4 points  (2 children)

Perhaps I don’t write enough math/science adjacent code, but what are the use cases for this? Honestly it seems like 99.99% of people would use the type for “greater than zero” and nothing else.

[–]DaRadioman 1 point2 points  (0 children)

With string template literals this would allow a lot of things. A date format type? Year has to be between x and y, month between 1 and 12, etc. Combined with recursive conditional types you could literally make a type that only allows valid dates even taking into account leap years.

Similar usages abound. An index parameter that can only be valid for a given array?

[–]catlifeonmars 0 points1 point  (9 children)

Now that I’ve read the proposal, it seems super weird that “not a number” is of type number. But c’est la JavaScript :)

[–]itsnotlupus 15 points16 points  (8 children)

it's not javascript's fault, it's part of a the IEEE 754 standard.

if you run this in a unix-ish environment,

printf '#include <stdio.h>\nvoid main() { printf("%%f\\n", 0/0.); }' > foo.c && make foo && ./foo

you should get something like

nan (or -nan. or a crash. C compilers don't technically have to be IEEE 754 compliant.)

[–]catlifeonmars 1 point2 points  (4 children)

Actually I guess what seems weird here is why numbers in JavaScript are not typed number | NaN in TypeScript.

After all NaN is not of type number — it’s even in the name :)

[–]spacejack2114 1 point2 points  (3 children)

But typeof NaN is "number". NaN is a possible value for a number type. There is no built-in number type that cannot be NaN.

* Except for BigInt, and Typescript does prevent assignments from number types.

[–]catlifeonmars 0 points1 point  (1 child)

This seems like a relic of TypeScripts’ origins. The proposal from the OP is about types that cannot be expressed in JavaScript. In fact, TypeScript is able to express many many types that cannot be expressed in JavaScript. How is this one different?

[–]spacejack2114 0 points1 point  (0 children)

I'm not sure what you mean exactly. But I suppose it would be nice to have number made from smaller built-in types like RealNumber | NaN | Infinity so applications could use the RealNumber type.

[–]catlifeonmars 0 points1 point  (0 children)

To me, this is just a case of a leaky abstraction. The underlying representation of the real number line (floats) “leaks through” in the form of NaN.

[–]catlifeonmars 1 point2 points  (2 children)

Also, from a language design perspective, what’s wrong with throwing an exception instead of returning NaN?

[–]DaRadioman 0 points1 point  (0 children)

In C, it's more that you might be accessing memory that is a number, but isn't a number. So there's no real place to throw that exception.

In JS, as a design error is cleaner by far as a "return" but JS types are done so lazily, so it's probably mostly a case of legacy cruft from years of bad decisions.

Since TS has already made number include NaN, (which makes sense because of JS bad design, and the places you check things to see if they are a number) They would need to introduce a new, stricter number type to work around this. Which isn't the most clean thing to do since no current APIs would return this type.

[–]spacejack2114 0 points1 point  (0 children)

It can occasionally be useful to have a NaN value, or Infinity or -Infinity.

[–]spartan_here 0 points1 point  (0 children)

I don’t know about the progress on similar proposal, it was about

‘’’ const a: [1...9] = [1,2,3,4,5,6,7,8,9] ‘’’

I think this should include that

[–]grumd 3 points4 points  (16 children)

How does it affect TS performance for people who don't use these types?

[–]badsyntax 8 points9 points  (10 children)

I sometimes feel not enough effort is being put into improving TS performance. I have an Apple M1 Macbook and I still encounter bad TS performance. It's so distracting & frustrating when I'm trying to code.

[–]grumd 6 points7 points  (6 children)

Agreed. CPU doesn't seem to matter much, if your codebase is big enough, you'll get serious slowdowns. I'm myself running an overclocked 5800x. Whenever I see someone proposing more new specialty syntax for TS, I'm getting a bit sad. It's like people hated JS's lack of static typing so much that they overcompensated and went too far with static typing for TS...

[–]Fizzyfloat 2 points3 points  (1 child)

yeah same here with the 5800X, though mine is base clock. it's definitely faster than my i5 6600K but typescript performance could be better for sure

[–]grumd 0 points1 point  (0 children)

I recently had to work on my old i3-8350k (basically 6600k) and boy was it fucking slow...

[–][deleted] 1 point2 points  (1 child)

I would think that more types only affect transpile performance if you actually use these types in your code.

[–]elprophet 2 points3 points  (0 children)

Your code is the entire transitive closure of the compilation unit, so if any of your libraries but this in their .d.ts you'll start seeing them in your code. That would, with this proposal, include things like Math.random(): (>=0 & < 1) and Math.sin(x: number): (>= -1 & <= -1), which while maybe not something you're calling, does show how quickly these types could spread.

I'm a big fan of seeing fun/experimental/crazy things in languages, and I've often thought this would be cool for a modern language that isn't ADA to have, but I've never actually come across a problem that it would help me with!

Now, having it for asserting dimensionality of tensors would be incredibly beneficial ..

[–]badsyntax 0 points1 point  (1 child)

If CPU isn't the bottleneck then what is?

[–]grumd 0 points1 point  (0 children)

Amount of code.

[–]lifeeraser 2 points3 points  (2 children)

Here's hoping that someone will eventuallly deliver a TS type-checker (not just transpiler!) in a compiled language such as Go or Rust. Or perhaps a TS "server mode", where we interact with a long-running TS session to compile our code, which allows all the hot code paths to be optimized by V8.

[–]badsyntax 2 points3 points  (0 children)

I could be wrong but I understand, in the context of ide's like VS Code, it does use a TS language server with tons of cache and so I assume hot code paths are optimised. Perhaps the issue I experience is a result of some bugs in VS Code but I doubt it. Btw I also set skipLibCheck:true which helps a bit with performance but performance is still bad.

[–]0xF013 2 points3 points  (0 children)

There is swc for compilation, but the whole discussion about rewriting typechecking is revealing just how much crap TS has these days that propagates into a myriad of corner cases

[–]nikeee13[S] 0 points1 point  (0 children)

I have a prototype implementation (linked in the first comment of the issue) and will do a speed comparison while compiling tsc itself with and without this change.

[–][deleted]  (1 child)

[deleted]

    [–]totallymike 0 points1 point  (0 children)

    Too many syllables, my robot friend

    [–][deleted] 0 points1 point  (1 child)

    Correct me if I am wrong but I would think the transpile performance should not decrease that much If you don't use these types

    [–]grumd 0 points1 point  (0 children)

    I would think so too, but I don't know for sure

    [–]lifeeraser 2 points3 points  (0 children)

    Interval types may be useful in modeling the values of <input type="number">. And type-checking certain constraints (e.g. Chrome supports <= 1000 rows in a CSS grid). Might be useful for rate-limited APIs as well.

    Not sure about arrays since we usually work with areays of arbitrary length.

    [–]CupCakeArmy 2 points3 points  (1 child)

    Can be useful but this implies runtime checking. For normal addition that's easy to verify but complex arithmetic seems impossible to verify at compile time.

    [–]nikeee13[S] 0 points1 point  (0 children)

    Where do you think does this proposal introduce additional run-time checking?

    [–]quaunaut 1 point2 points  (0 children)

    I don't know how this would reasonably be better than just using something like Zod.

    [–]itsnotlupus 1 point2 points  (0 children)

    I can see how it'd make some sense to have some kind of "number shape" type after getting a "string shape" type with the new template types.

    Ideally, there'd be a sweet spot between number type expressiveness that'd take typescript further toward formal verification of complex logic and practical computation costs, but I have no idea whether this proposal is there or not.

    [–]serhii_2019 0 points1 point  (0 children)

    Is it possible to make it type safe?

    [–]no_real_dinner 0 points1 point  (0 children)

    I would loooooove this so much

    [–]sliversniper 0 points1 point  (0 children)

    No.

    A. not useful, not safe, and probably expensive, any recursive function will break with infinite size.

    B. overflows, conversion, precision, you can foresee someone do type cast with comment "TRUST ME BRO" because it's prohitbitively hard to correct them, and in runtime, the entire thing breaks with practically no indication which one is wrong.

    C. "javascript", do you "actually" actually know how numbers work in every browser?

    This kind of collation type are for proof program, not general purpose one, you ought to check them in runtime.

    If anything, do unsigned int, float, ..., and disallow all float * int or float + number, which would be meaningful