I have read the FAQ and looked for duplicate issues.
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.)
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.
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.)
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;
My suggestion meets these guidelines:
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 an integer
value, eg. with math operations (as far as internally conversion to integers and back takes place):
let x: integer = 1; // you expect it's value to always be an integer
x = 5 / 2; // compile error
x = Math.floor(5 / 2); // ok
I think dividing number
into integer
and float
is enough, while dealing with NaN
and Infinity
you need to look into the values for operators like /
, while dealing with integer
-float
, you need only types.
Shouldn't the terminology be double
and not float
?
I believe JS numbers are actually double precision.
Nitpick aside, an integer
type that is backed by number
and not bigint
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 by number
.
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
and OFFSET
clauses would have benefited from an integer
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 by number
but I disagree with how the OP wants the type to be defined.
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 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,
type PotentiallyNonFiniteInteger = integer|Infinity|-Infinity|NaN;
With my proposal, number
would be the same as finiteNumber|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 the Number.isInteger()
convention,
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger#Polyfill
Number.isInteger = Number.isInteger || function(value) {
return typeof value === 'number' &&
isFinite(value) &&
Math.floor(value) === value;
};
[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 if strictNanChecks
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 of Inifinity,-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.
It will be great also suggest replace x === NaN
to Number.isNaN(x)
or isNaN(x)
during diagnostics because x === NaN
always false and 100% mistake.
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 Number.isFinite()
, I suggest use finite
instead of realNumber
as the name of the type for real numbers.
When I started really learning TS, I expected that at least NaN
and Infinity
would have their own types, but alas no, they do not, they are simply of the type number
.
Also, arrays really ought to be Array<T> = { [key: integer]: T }
.
We can all agree this is great, totally bug-free cde, right?
const temp: string = ["temporary value"][Infinity];
console.log(temp);
Who could have expected undefined
!?
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, any NaN
checking that follows is mostly going to be noise. For example, if you write Math.log(arr.length)
, this can never return NaN
, but we only know this because arr.length
can't be negative. Many other functions work in a similar way - to provide these NaN
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 unexpected NaN
s due to logic errors.
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.
Most helpful comment
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 internally conversion to integers and back takes place):