TypeScript: Add a --strictNaNChecks option, and a NaN / integer / float type to avoid runtime NaN errors
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.
About this issue
- Original URL
- State: open
- Created 6 years ago
- Reactions: 357
- Comments: 41 (6 by maintainers)
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.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
NaN
typing. We discussed this at length and couldn’t come up with any plausible design where the complexity introduced here would provide sufficient value in most use cases.The only reasonably common way to get a
NaN
with “normal” inputs is to divide by zero; programs that do standard arithmetic should be on guard for this and add appropriate checks the same way you would for handling file existence, lack of network connectivity, etc (i.e. things that are common errors but not enforced by a type system). Once a well-formed program correctly guards its division, anyNaN
checking that follows is mostly going to be noise. For example, if you writeMath.log(arr.length)
, this can never returnNaN
, but we only know this becausearr.length
can’t be negative. Many other functions work in a similar way - to provide theseNaN
s in useful places we’d have to encode a large amount of data about possible values for numbers (integers, positive, nonnegative, less than 2^32, etc) all over the type system, which is a ton of work that everyone would have to reason about from that point forward for the sake of the relatively small number of programs that encounter unexpectedNaN
s due to logic errors.Shouldn’t the terminology be
double
and notfloat
? I believe JS numbers are actually double precision.Nitpick aside, an
integer
type that is backed bynumber
and notbigint
would have proven immensely useful for my use case.My use case is outlined in the below link, https://github.com/AnyhowStep/tsql/blob/adbfcf666ef71be4b6c03567a8d14a88ab699d7c/doc/00-getting-started/01-data-types.md#signed-integer
Basically, SQL has
SMALLINT,INT,BIGINT
types that correspond to 2,4,8 byte signed integers.The 2 and 4 byte signed integers could have been represented by an
integer
type backed bynumber
.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
bigint
.Also, the
LIMIT
andOFFSET
clauses would have benefited from aninteger
type, so I can guarantee that numbers with fractional parts are not passed as arguments (during compile-time, anyway)This could have been solved with branding.
However, I’m strongly of the opinion that branding should be a last resort. Which is why I like this proposal and the range-type proposals so much. Being able to express what we want with only built-in types reduces dependencies on external libraries for complex branding.
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
{int : void}
as the integer brand, library B might use{integer : void}
as the integer brand, etc)I like the idea of having an
integer
type backed bynumber
but I disagree with how the OP wants the type to be defined.I would prefer,
I do not like the idea of Infinity, -Infinity, and NaN being part of the
integer
type. It is very unintuitive and usually not what I (and I assume others) would want.If someone wants the original proposal, they can just create their own type alias,
With my proposal,
number
would be the same asfiniteNumber|Infinity|-Infinity|NaN
.We would get the following type guards,
isFinite(x) : x is finiteNumber
isNaN(x) : x is NaN
Number.isInteger(x) : x is integer
Also, another reason to prefer my proposed definition for
integer
is that it follows theNumber.isInteger()
convention, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger#Polyfill[UPDATE] I wrote an update earlier on but it got lost to the void because GitHub error’d. Ugh. I am retyping my update below.
Anyway, I re-read the OP and realized I had misunderstood parts of it. The
integer
proposal I disagree with would only be applied ifstrictNanChecks
is turned off.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
number
type.If anyone uses
integer,finiteNumber,Infinity,-Infinity,NaN
(new types), they should be forced to acknowledge the possibility ofInifinity,-Infinity,NaN,etc.
values creeping in to their code and handle it accordingly (with type guards, run-time errors, etc.)If they do not want to have the compiler warn them about potentially undesirable values, they can stick to using
number
and live blissfully unaware of impending doom.Another point that doesn’t seem to have been brought up yet is that allowing
NaN
as a type literal allows for type narrowing when checking for falsy values:I may have missed something but I don’t see how having “bigint” helps in any way in differentiating integers vs floating point numbers? 🤷♂️ As far as I know, bigint is for (very) large numbers (MDN: “represent whole numbers larger than 2^53 - 1”). Bigint values are fully incompatible with both numbers and Math functions, cannot be included in JSON, and probably more caveats that I don’t know of.
Appart from that,
Number.isInteger
has been part of the language since … I don’t know, but quite some time now. It seems fair that typescript would be able to tell the difference between a number and an integer.I understand that it may be a lot of work, but integers are a thing. How can people even be ok with the idea of using “number” to type durations, quantities, indexes, or any whole number. 😕
Claiming that developers can handle the thing themselves (but without typescript) sounds wrong to me.
[I will be very incisive here, not because I like to, but because I believe it is unfortunately necessary.]
@c-dinkel there are two important things to respond to the “no compelling scenario” argument / decision method:
It is fundamentally a bug-promoting mentality. Waiting for a compelling scenario to appear before trying to get a fundamental aspect of the language right… flies in the face of the sanity of the language. It’s exactly saying “let’s wait for unfinished foundations to cause the practical problems they were bound to, before we consider taking action”. It is, technically, willful ignorance of how much practical benefit ensues from mathematical elegance. For these reasons, I call its past users to please reconsider before using that argument / decision method.
I am most confused that the lead architect of TypeScript either:
And, yes, “compelling scenarios” have been provided. But my point is that it’s crazy that any of that should be needed: the mere idea of unchecked division by zero should suffice.
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.
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.
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:
I think it’s not a big enough deal though since TypeScript is already unsound—
['a'][1].length
has no type errors but throws aTypeError
because having undefined checks for array access is annoying—and the type systems of most other languages such as Rust don’t check for integer overflows, so there’s not really a need to require checks on the programmer’s side for large floating point integersAlso, using the name
float
ordouble
to mean “not NaN nor an Infinity” might be misleading because NaN and the Infinities are valid floating point values in other programming languages. I’m not really sure what values would be arealNumber
but not afloat
, but I think usingrealNumber
/real
orfinite
would probably be a better name for a such a set of numbersWhile 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.
It is useful to be able to prove that a function does not return
NaN
. I think that needs no justification but here it is for completeness: this would allow, compile-time, to avoid hard-to-debug runtime crashes that are due to a division by zero (or other) that produces NaN, and causes a confusing error very far away from the code that creates thisNaN
value initially.It is useful to be able to tell the compiler we do want a function that can return
NaN
, exactly when we want to handle it later, not here, in a function whose type is exactly one that disallowsNaN
as a return value.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.
We need this in order to specify a function that has to return
NaN
on purpose in specific cases. If we returned another number, like0
, we could declare it as:but sadly we can currently not declare the function as returning:
so we have to fall back to the very loose definition
It would be greate to see this new rule in TypeScript 4.0
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.
And also
-0
should be there 😆The only thing that distiguishes
-0
from0
(i.e.+0
) isSameValue
equality, which is leveraged only inObject.is
, so I’m actually not sure it can be supported in TS (or worth it), but then we can’t say it’s completely “strict” number type checking if we don’t support-0
.I can’t believe this has not been implemented yet.
NaN
is one of the most runtime-error-prone thing in JS, which could (and must) be avoided by static typing. That’s the very purpose of TypeScript, isn’t that?While in this particular case a value is needed that compares falsely to itself, there are other cases where
NaN
is necessary for other reasons. Imagine, for example, a numeric calculation that returns0 | 1 | 2 | 3 | NaN
, I’d really want to be able to express that.And no, I’m not willing to consider changing the runtime semantics of the program just to make
tsc
happy at compile time. Also, what if I’m trying to add types to an external library I don’t control?Since there is a function called
Number.isFinite()
, I suggest usefinite
instead ofrealNumber
as the name of the type for real numbers.It’s idiomatic in JavaScript to just use
1
instead of1.0
even in cases where it’s used as a real number rather than an integer (e.g. setting image quality, media volume, canvas shapes, etc). Having to use1.0
for floats is an idiom from some other programming languages that shouldn’t be forced onto TypeScript users for no good reason. We already have special syntax for bigints, anyways@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!
I believe this would help solve #47347 / #15135.
@yuhr also,
1 / -0 !== 1 / 0
.-0
exists in JS, it’s a thing.@yuhr perhaps the typescript developers need help with this because it’s too hard to do? Or financial support if microsoft doesn’t provide enough?
More realistically, most likely it’s precisely because corporate priorities are orthogonal to software quality that this is not on the roadmap. Many such projects (this includes Rust) tend to forget/tone down their initial goals like this: instead of failing the compile if the program isn’t provably safe, which was the initial objective, TS is becoming more of a linter which catches what it can and lets potential errors fly when it doesn’t know.
While we would want it to (at least have an option to) never accept programs that may crash, it is out of ignorance that more novice developers do not care, and simply relay what management asks for: do dangerous things and deal with the consequences later.
Of course, wise developers would rather make sure, at compile time, that no errors are ever possible.
Novice developers, managers and the former under the influence of the latter think that a compromise is possible to reap a lot of benefits with minimum risk; and that’s why modern technology is plagued with bugs.
Well, it’s an all-or-therewillalwaysbeshenanigans choice. I was hoping TS might aim for complete correctness by doing all of the above; perhaps the answer is “not now”, then?
Easy comes with complications; simple comes with complexity.
I think dividing
number
intointeger
andfloat
is enough, while dealing withNaN
andInfinity
you need to look into the values for operators like/
, while dealing withinteger
-float
, you need only types.You can actually do even more than that: there’s int32 which is closed on
&
,|
,~
,^
,<<
, and>>
, and uint32 which is closed on<<<
and>>>
. These are totally NaN- and Infinity-safe:NaN << NaN
is 0 and1 << Infinity
is 1.I think this is unsolvable, as
a+a
is infinity and(a+a)-(a+a)
is NaN whena = Number.MAX_VALUE
(which is an integer). So NaNs and infinities can creep up into any ordinary integer-only code. You couldn’t call a function passing an arithmetic expression to an integer parameter without casting, so you’d have to do things likef((i+1) as integer)
, which kinda defeats the purpose of types.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:
NotFraction
, which would contain integers, infinities and NaNs – preserved by+
,-
,++
,--
,*
,|
,&
,^
,~
,NonnegativeNumber
, which would contain nonnegative finite numbers (including -0) and positive infinity – preserved by+
and++
(in particular, not preserved by*
,/
or**
, as0*Infinity
,0/0
and1**Infinity
are NaN),NonpositiveNumber
(including +0), preserved by+
and--
,PositiveNumber
, which would contain positive finite numbers (excluding +0) and positive infinity – preserved by+
,++
and*
(in particular, not preserved by**
, as1**Infinity
is NaN),NegativeNumber
(excluding -0), preserved by+
and--
, and withNegativeNumber * NegativeNumber = PositiveNumber
,NonnegativeNotFraction = NotFraction & NonnegativeNumber
, etc.NaN
containing NaN, just soFalsy
could be defined.Note there’s no way to guarantee lack of infinity, and no way to have subtraction, division or modulo without NaNs. Also, I don’t think those types should ever be inferred.
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.
@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
NaN
as a type literal, just as an annotation. NaNs are quite useful signaling tools in numeric code.At the very least,
NaN
,Infinity
and-Infinity
should be valid and correctly handled in type positions, since TypeScript can and will emitInfinity
and‑Infinity
in type positions: https://github.com/microsoft/TypeScript/issues/42905.This would also make it possible for TypeScript to produce an error for code like the following:
Then TypeScript warns that
=== NaN
will always returnfalse
and!== NaN
will always returntrue
.Similarly,
Number.isNaN
could then be defined as:There’s also real world code that relies on returning
NaN
in acatch
clause: https://github.com/engine262/engine262/blob/ab4baf1b1faeae35da022aea6c60a6a824822167/src/abstract-ops/type-conversion.mjs#L404-L415, because the error is handled elsewhere: https://github.com/engine262/engine262/blob/ab4baf1b1faeae35da022aea6c60a6a824822167/src/abstract-ops/type-conversion.mjs#L386-L394.