Add a --strictNaNChecks option, and a NaN / integer / float type to avoid runtime NaN errors
See original GitHub issueI 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.
Issue Analytics
- State:
- Created 5 years ago
- Reactions:287
- Comments:32 (6 by maintainers)
Top GitHub Comments
It would actually be great to have
integer
type. As discussed in #195 and #4639, it might help to force developers to do strict conversions and checks when you expect to get aninteger
value, eg. with math operations (as far as under the hood of JS engines, an internal conversion to integers and back takes place):It will be great also suggest replace
x === NaN
toNumber.isNaN(x)
orisNaN(x)
during diagnostics becausex === NaN
always false and 100% mistake.