Add a --strictNaNChecks option, and a NaN / integer / float type to avoid runtime NaN errorsΒ #28682
Description
I have read the FAQ and looked for duplicate issues.
Search Terms
- NaN
- NaN type
- Integer type
Related Issues
- #21279: strictNullChecks safeguards against null and undefined, but not NaN
- #15135: NaN, Infinity and -Infinity not accepted in number literal types
- #195: Suggestion: int type
- #4639: Proposal: int types
- BigInt is scheduled for TS 3.0 - #15096 - Support for TC39 "BigInt: Arbitrary precision integers in JavaScript" proposal
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 return NaN
. These NaN
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 the NaN
came from.
I would like TypeScript to provide a better way of preventing runtime NaN
errors, by ensuring that an unhandled NaN
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 returning NaN
(but these are outside the scope of this issue.)
Use Cases / Examples
const testFunction = (a: number, b: number) => {
if (a > b) {
return;
} else if (a < b) {
return;
} else if (a === b) {
return;
} else {
throw new Error("Unreachable code");
}
}
testFunction(1, 2);
testFunction(1, 0 / 0);
testFunction(1, Math.log(-1));
testFunction(1, Math.sqrt(-2));
testFunction(1, Math.pow(99999999, 99999999));
testFunction(1, parseFloat('string'));
A programmer might assume that the Unreachable code
error could never be thrown, because the conditions appear to be exhaustive, and the types of a
and b
are number
. It is very easy to forget that NaN
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 exclude NaN
. The return types of built-in JavaScript functions and operations would be updated to show which functions can return NaN
, and which ones can never return NaN
. A call to !isNaN(a)
would narrow down the type and remove the possibility of NaN
.
Here are some possible types that would make this possible:
type integer
type float
type NaN
type Infinity
type number = integer | float | NaN | Infinity // Backwards compatible
type realNumber = integer | float // NaN and Infinity are not valid values
(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:
const testFunction = (a: integer, b: integer) => {
if (a > b || a < b || a === b) {
return;
} else {
throw new Error("Unreachable code");
}
}
// Ok
testFunction(1, 2);
// Type error. TypeScript knows that a division might produce a NaN or a float
testFunction(1, 0 / 0);
const a: integer = 1;
const b: integer = 0;
const c = a + b; // inferred type is `integer`. Adding two integers cannot produce NaN or Infinity.
testFunction(1, c); // Ok
const d = a / b; // inferred type is `number`, which includes NaN and Infinity.
testFunction(1, d); // Type error (number is not integer)
const e = -2; // integer
const f = Math.sqrt(e); // inferred type is: integer | float | NaN (sqrt of an integer cannot return Infinity)
const g: number = 2;
const h = Math.sqrt(g); // inferred type is number (sqrt of Infinity is Infinity)
testFunction(1, h); // Type error. `number` is not compatible with `integer`.
if (!isNaN(h)) {
// The type of h has been narrowed down to integer | float | Infinity
testFunction(1, h); // Still a type error. integer | float | Infinity is not compatible with integer.
}
if (Number.isInteger(h)) {
// The type of h has been narrowed down to integer
testFunction(1, h); // Ok
}
When the --strictNaNChecks
option is disabled (default), then the integer
and float
types would also include NaN
and Infinity
:
type integer // Integers plus NaN and Infinity
type float // Floats plus NaN and Infinity
type number = integer | float // Backwards compatible
type realNumber = number // Just an alias, for forwards-compatibility.
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 calls a / 2
, there is no need to warn about NaN
(unless a
is a number
type that could potentially be NaN
.) NaN
is only possible for 0 / 0
. So if either the dividend or the divisor are non-zero numbers, then the NaN
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:
type nonZeroNumber = Exclude<number, 0>
type nonZeroRealNumber = Exclude<realNumber, 0>
type nonZeroInteger = Exclude<integer, 0>
type nonZeroFloat = Exclude<float, 0>
If the dividend and divisor type both match nonZeroInteger
, then the return type would be nonZeroFloat
. So you could test any numeric literal types against these non-zero types. e.g.:
const a = 2; // Type is numeric literal "2"
// "a" matches the "nonZeroInteger" type, so the return type is "nonZeroFloat"
// (this excludes Infinity as well.)
// (Technically it could be "nonZeroInteger", or even "2" if TypeScript did
// constant propagation. But that's way outside the scope of this issue.)
const b = 4 / a;
Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.
Activity