-
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
Add a --strictNaNChecks option, and a NaN / integer / float type to avoid runtime NaN errors #28682
Comments
strictNaNChecks
option, and aNaN
type
It would actually be great to have let x: integer = 1; // you expect its value to always be an integer
x = 5 / 2; // compile error
x = Math.floor(5 / 2); // ok |
I think dividing |
Shouldn't the terminology be Nitpick aside, an My use case is outlined in the below link, Basically, SQL has The 2 and 4 byte signed integers could have been represented by an However, I wanted to eliminate a large class of errors where floating-point values are used instead of integer values. So, I had no choice but to also represent 2 and 4 byte signed integers with Also, the This could have been solved with branding. However, I'm strongly of the opinion that branding should be a last resort. If a brand is used, it increases the chance of downstream users having to use multiple differing brands that mean the same thing. (Library A might use I like the idea of having an I would prefer, //Infinity is now a literal type
declare const inf : Infinity;
//-Infinity is now a literal type
declare const negInf : -Infinity;
//NaN is now a literal type
declare const nan : NaN;
//No change to this type
type number;
type finiteNumber = number & (not (Infinity|-Infinity|NaN));
type integer; //All **finite numbers** where Math.floor(x) === x I do not like the idea of Infinity, -Infinity, and NaN being part of the If someone wants the original proposal, they can just create their own type alias, type PotentiallyNonFiniteInteger = integer|Infinity|-Infinity|NaN; With my proposal, We would get the following type guards,
Also, another reason to prefer my proposed definition for Number.isInteger = Number.isInteger || function(value) {
return typeof value === 'number' &&
isFinite(value) &&
Math.floor(value) === value;
}; [UPDATE] Anyway, I re-read the OP and realized I had misunderstood parts of it. The I am against the new flag being introduced. New flags should only be introduced when intentionally breaking changes are introduced. These new types would not break the current behaviour of the If anyone uses If they do not want to have the compiler warn them about potentially undesirable values, they can stick to using |
It will be great also suggest replace |
Has this issue been picked up by the maintainers? It's surprising to me that a type-checking compiler for JavaScript wouldn't try to prevent one of the most notorious type issues prevented by nearly every other language, dividing by zero (not sure if TS would need zero and non-zero subtypes in order to be able to typeguard a valid division). EDIT: I guess I should add that other languages don't necessarily prevent this at compile time, but they throw errors when it happens. JavaScript fails silently, so it would be great for TypeScript to be able to be able to detect this. But I understand that it requires some significant modifications to the type system. |
It would be greate to see this new rule in TypeScript 4.0 |
Since there is a function called |
When I started really learning TS, I expected that at least Also, arrays really ought to be We can all agree this is great, totally bug-free code, right? const temp: string = ["temporary value"][Infinity];
console.log(temp); Who could have expected |
n.b. Integer types have already been decided as out of scope (especially now that BigInt is a thing), so what follows only refers to The only reasonably common way to get a |
Well, it's an all-or-therewillalwaysbeshenanigans choice. Easy comes with complications; simple comes with complexity. |
At the very least, This would also make it possible for TypeScript to produce an error for code like the following: declare var foo: number;
// @ts-expect-error
if (foo === NaN) {
// ...
} Then TypeScript warns that Similarly, interface NumberConstructor {
isNaN(number: unknown): number is NaN;
} There’s also real world code that relies on returning |
We need this in order to specify a function that has to return
but sadly we can currently not declare the function as returning:
so we have to fall back to the very loose definition
|
@c-dinkel Are there any particular reasons that you're using NaN for that value? Could you elaborate on what your function actually does, and in what case it is returning NaN? Is it your function or just a type definition for an arbitrary JS function? Would you be willing to consider |
I have encountered code like @c-dinkel’s in the wild as well, so having a |
While I don't think it is good to implement such fundamental language features on an as-needed basis[1], the question @crimsoncodes0 asks does deserve a proper answer, which was not given in the reply above.
Errare humanum est. Without strict checks, Murphy's law reigns! [1] This is because all code written before the availability of basic features in the language will have to use workarounds or be unsafe, and will need to be rewritten when the language fixes that flaw, which it now needs to do with some amount of backwards compatibility with the workarounds in question. All that code working around missing language features will not be refactored using the new feature right away, or at all, because that has a cost. All in all, introducing language features as the need arises creates technical debt orders of magnitude higher, as well as "good practices" that will be obsoleted by introducing the language features proper. |
While in this particular case a value is needed that compares falsely to itself, there are other cases where And no, I'm not willing to consider changing the runtime semantics of the program just to make |
Regardless of whether an integer type is added, I don't think this form of NaN checking, where two operands known to not be NaN produces a non-NaN number, would be perfect. For example, adding two integers doesn't necessarily produce another integer: const int: number = 1e308
if (Number.isInteger(int)) { // This is true
// int: integer
const sum: integer = int + int // At runtime, Infinity
const difference: integer = sum - sum // At runtime, NaN
} I think it's not a big enough deal though since TypeScript is already unsound— Also, using the name |
@crimsoncodes0 you asked all these clarification questions about our use case last year, were they all answered in my comment above? Have you been satisfied by my explanation? If so, what is your suggestion in our case? |
@SheepTester made an excellent point above... there's no sound way to have an integer type or finite type without checking nearly every operation and/or doing sophisticated range analysis. The four main binary floating-point operations can over- or underflow; the only practicable operations to check are those which cast to signed 32-bit. Despite this limitation, I would really like to see |
@anematode well there are two objectives; I'll concentrate on avoiding runtime type errors caused by NaN. Checking every operation is necessary, and this is exactly what TypeScript does by construction: operations (operators) are functions, and TS checks that function signatures match their calls. That's actually already the main selling point of TS. Thankfully, new compiler features like range analysis can remain out of the picture to do this. This can be implemented only by introducing a more stringent number type or compiler option and adding a few signatures, which will in turn make TS reject programs that lack the proper guard conditions before potentially dividing by zero and so on. The only reason I can imagine TS maintainers getting cold feet at the idea, is that at first glance they might think it will result in several operation modes and break backwards compatibility, but with new types it doesn't have to! |
When dealing with user input, it's not uncommon to do There's also just that I don't like to have to do |
Since numbers are out of scope of TS, here's a playground with a potential workaround. I'm currently writing a library to .d.ts polyfill all the methods that can potentially return NaN, along with |
I think this is pretty broken atm, type Falsy = 0 | '' | false | null | undefined | -0 | 0n // | typeof NaN <-- doesn't work
Also, to guard against truthy values, I had to create the export function assertFalsy(value: unknown): asserts value is Falsy {
if (value) throw new AssertionError('Expected falsy value')
} Can't we just write Context of why this matters - Probably in new code you would add |
Is this anywhere close to being fixed? |
Nope, after lots of time and many issues this is apparently not considered an actual issue by the maintainers. If you absolutely need this fixed, consider forking TypeScript. |
I think this is unsolvable, as Alternatively, you could define types of arithmetic operations to be unsound (i.e. return integer for integers and ignore overflows), but I don't think Typescript needs any more unsoundness. The only things that could be safely done are:
Note there's no way to guarantee lack of infinity, and no way to have subtraction, division or modulo without NaNs. Of course I could have made some mistakes above, this is not a serious proposal, as I don't think those types would be very useful. |
You can actually do even more than that: there's int32 which is closed on |
I just want to be able when I write 1.0 and its not suppossed to be float to get an error in typescript and i want if the type is float to have to make it 1.0. Is that not possible? |
It's idiomatic in JavaScript to just use |
The "good reason" is allowing users to take advantage of a type system. A frequent problem I'm running into is that, with WASM, there's no way to signal to consumers in JS whether an interface accepts signed/unsigned integers, floats, etc. Users have to read the source or guess and check as TS can't provide any useful info beyond "it's a number of some sort", which is a shame. Being able to specify, precisely, what kind of number is allowed is really important as TS branches out to describe interfaces beyond plain JS. |
I wonder if the type system is the right place for that though. Constraining WASM interfaces is a complex problem, not fitting the weird JS type system. As previously said, you cannot have an integer type that doesn't immediately dissolve into nonsense. So what about: compile-time preconditions for function parameters, which can reject incorrect constant arguments? This would allow for many other compile-time checks, like "require a prime number", "require a list of length divisible by 4", "require two fields of the object to be different" and so on. Extend function types with another component: precondition, which is an arbitrary TS expression. Only a reasonable subset of TS needs to be supported, and its environment contains only the parameters and the standard library. When typechecking a call to a function with a precondition, plug all the parameters into the expression, assuming unknown value for non-constant parameters, and evaluate it taking into account interactions between known and unknown values (so it cannot be just a simple JS eval). If the result is a falsy constant, report compilation failure: // WARNING: REALLY SILLY NOT-ACTUALLY-A-PROPOSAL FOLLOWS:
declare function requires_i32(i:number, j:number):void havingPrecondition (i === (i|0) && j === (j|0)) ;
export let not_a_constant = 1.2;
requires_i32(0, 0); // ok
requires_i32(0, not_a_constant); // ok, because who knows what's inside that variable
// true && unknown_value is unknown_value according to the precondition evaluator,
// which is not a falsy constant, therefore it compiles fine
requires_i32(0, 3.14); // definitely not ok
requires_i32(not_a_constant, 3.14); // definitely not ok – even if we cannot evaluate the precondition like normal code,
// unknown_value && false is still false according to the precondition evaluator Just a throw-away idea, I think it's a bit too complicated to implement. |
This would help a lot to see parseInt problems on code, as we just managed to create bug on our code as developers still expecting parseInt actually throw Type error if not valid (or expect undefined). Of course we can build custom parseInt function to handle this, but on large codebase actual NaN return type would already help to identify those issues. |
Even if they cannot be implemented on most of build in operators, they are more than useful in libraries and custom utilities. Even an ability for it to understand that integer is assignable to float and float is assignable to number is a huge improvement so you don't need to make a branded types yourself and cast each time you write const a = 5 as integer to pass it into function. isSafeInteger, isInteger, isFinite, isNaN will work like a typeguard, Math.floor, also in place where NaN can be, you can use NaN || 0. |
Also it will be good to restrain from using numbers out of the Date range, out of Radix range, out of toFixed argument range and array range and safeNumber range, at least on a level of constants, because simple for loop with increment can turn into infinite loop |
Refinement types would be amazing (actually, more than just "amazing"). See https://arxiv.org/pdf/1604.02480.pdf But (as previously mentioned) the predicate language must be a subset of TS, to forbid Turing-Completeness (we already have it #14833), or at least remove some "useless" functions and operator semantics (for basic sanity). Or maybe the predicate-lang could be a different lang altogether (other langs did something similar with macros) |
I have read the FAQ and looked for duplicate issues.
Search Terms
Related Issues
Suggestion
NaN
has been a big source of errors in my code. I was under the impression that TypeScript (and Flow) could help to prevent these errors, but this is not really true.TypeScript can prevent some
NaN
errors, because you cannot add a number to an object, for example. But there are many math operations that can returnNaN
. TheseNaN
values often propagate through the code silently and crash in some random place that was expecting an integer or a float. It can be extremely difficult to backtrack through the code and try to figure out where theNaN
came from.I would like TypeScript to provide a better way of preventing runtime
NaN
errors, by ensuring that an unhandledNaN
value cannot propagate throughout the code. This would be a compile-time check in TypeScript. Other solutions might be a run-time check added with a Babel plugin, or a way for JS engines to throw an error instead of returningNaN
(but these are outside the scope of this issue.)Use Cases / Examples
A programmer might assume that the
Unreachable code
error could never be thrown, because the conditions appear to be exhaustive, and the types ofa
andb
arenumber
. It is very easy to forget thatNaN
breaks all the rules of comparison and equality checks.It would be really helpful if TypeScript could warn about the possibility of
NaN
with a more fine-grained type system, so that the programmer was forced to handle these cases.Possible Solutions
TypeScript could add a
--strictNaNChecks
option. To implement this, I think TS might need to add some more fine-grained number types that can be used to excludeNaN
. The return types of built-in JavaScript functions and operations would be updated to show which functions can returnNaN
, and which ones can never returnNaN
. A call to!isNaN(a)
would narrow down the type and remove the possibility ofNaN
.Here are some possible types that would make this possible:
(I don't know if
realNumber
is a good name, but hopefully it gets the point across.)Here are some examples of what this new type system might look like:
When the
--strictNaNChecks
option is disabled (default), then theinteger
andfloat
types would also includeNaN
andInfinity
:I would personally be in favor of making this the default behavior, because
NaN
errors have caused me a lot of pain in the past. They even made me lose trust in the type system, because I didn't realize that it was still possible to run into them. I would really love to prevent errors like this at compile-time:This error is from a fully-typed Flow app, although I'm switching to TypeScript for any future projects. It's one of the very few crashes that I've seen in my app, but I just gave up because I have no idea where it was coming from. I actually thought it was a bug in Flow, but now I understand that type checking didn't protect me against
NaN
errors. It would be really awesome if it did!(Sorry for the Flow example, but this is a real-world example where a
NaN
type check would have saved me a huge amount of time.)Number Literal Types
It would be annoying if you had to call
isNaN()
after every division. When the programmer callsa / 2
, there is no need to warn aboutNaN
(unlessa
is anumber
type that could potentially beNaN
.)NaN
is only possible for0 / 0
. So if either the dividend or the divisor are non-zero numbers, then theNaN
type can be excluded in the return type. And actually zero can be excluded as well, if both dividend and divisor are non-zero.Maybe this can be done with the
Exclude
conditional type? Something like:If the dividend and divisor type both match
nonZeroInteger
, then the return type would benonZeroFloat
. So you could test any numeric literal types against these non-zero types. e.g.:Checklist
My suggestion meets these guidelines:
The text was updated successfully, but these errors were encountered: