Repository navigation
Add a --strictNaNChecks option, and a NaN / integer / float type to avoid runtime NaN errors #28682
Description
Activity
- changed the title
[-]Add a `strictNaNChecks` option, and a`NaN` type[/-][+]Add a --strictNaNChecks option, and a NaN / integer / float type to avoid runtime NaN errors[/+]on Nov 27, 2018 - addedSuggestionAn idea for TypeScriptAn idea for TypeScriptIn DiscussionNot yet reached consensusNot yet reached consensusDomain: Literal TypesUnit types including string literal types, numeric literal types, Boolean literals, null, undefinedUnit types including string literal types, numeric literal types, Boolean literals, null, undefined
on Nov 27, 2018 It would actually be great to have
integertype. As discussed in #195 and #4639, it might help to force developers to do strict conversions and checks when you expect to get anintegervalue, eg. with math operations (as far as under the hood of JS engines, an internal conversion to integers and back takes place):let x: integer = 1; // you expect its value to always be an integer x = 5 / 2; // compile error x = Math.floor(5 / 2); // ok
Reacted by Sindre Sorhus, Nick Clifford, NN, Jacob Pratt, Nathan Broadbent, Jake Coppinger, Théo FIDRY, Jeremy B, Demulier--Chevret Tom, JoBrad and 24 moreI think dividing
numberintointegerandfloatis enough, while dealing withNaNandInfinityyou need to look into the values for operators like/, while dealing withinteger-float, you need only types.Reacted by Miles B Huff, Etienne Maheu, Gulg and Fabian SchererShouldn't the terminology be
doubleand notfloat?
I believe JS numbers are actually double precision.Nitpick aside, an
integertype that is backed bynumberand notbigintwould 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-integerBasically, SQL has
SMALLINT,INT,BIGINTtypes that correspond to 2,4,8 byte signed integers.The 2 and 4 byte signed integers could have been represented by an
integertype 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
LIMITandOFFSETclauses would have benefited from anintegertype, 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
integertype backed bynumberbut 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
integertype. 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,
numberwould be the same asfiniteNumber|Infinity|-Infinity|NaN.We would get the following type guards,
isFinite(x) : x is finiteNumberisNaN(x) : x is NaNNumber.isInteger(x) : x is integer
Also, another reason to prefer my proposed definition for
integeris that it follows theNumber.isInteger()convention,
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger#PolyfillNumber.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
integerproposal I disagree with would only be applied ifstrictNanChecksis 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
numbertype.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
numberand live blissfully unaware of impending doom.Reacted by Nathan Broadbent, yossarian, Felipe S. S. Schneider, btoo, Patrick, Egor Andreyuk, Konrad Pettersson, Mary Richelle and fsubalReacted by Ricardo Fernández SerrataReacted by Nathan Broadbent, Felipe S. S. Schneider and Mary RichelleIt will be great also suggest replace
x === NaNtoNumber.isNaN(x)orisNaN(x)during diagnostics becausex === NaNalways false and 100% mistake.Reacted by Victor Malov, Nathan Broadbent, Thomas, Niklas Mollenhauer, falsandtru, Ruben Bridgewater, Jason Kwok, Karl Tarvas, PavelKoroteev, Amit Beckenstein and 17 moreHas 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.
Reacted by Nathan Broadbent, Dmytro Demensky, DetachHead, julianstoerig, flexiworld and Miles B Huffcaptain-yossarian commented
on May 24, 2020 More actionsIt would be greate to see this new rule in TypeScript 4.0
Reacted by Niles Salter, Felipe S. S. Schneider, flexiworld and Perry26Since there is a function called
Number.isFinite(), I suggest usefiniteinstead ofrealNumberas the name of the type for real numbers.Reacted by ExE Boss, Alin Ciocan and Perry26RyanCavanaugh commented
on Oct 12, 2020 MemberMore actionsn.b. Integer types have already been decided as out of scope (especially now that BigInt is a thing), so what follows only refers to
NaNtyping. 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
NaNwith "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, anyNaNchecking 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.lengthcan't be negative. Many other functions work in a similar way - to provide theseNaNs 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 unexpectedNaNs due to logic errors.Reacted by Nathan Broadbent, Ben Lesh, Jacob, Simon Chan, while(true);, yossarian, Noah and chocolateboyReacted by Miles B HuffReacted by calimeroteknik, Sʜɪᴍᴜʀᴀ Yū, Jeffrey Meyer, flexiworld and Escape0707Reacted by while(true);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.
Reacted by Jarrod Davis and Miles B HuffAt the very least,
NaN,Infinityand-Infinityshould be valid and correctly handled in type positions, since TypeScript can and will emitInfinityand‑Infinityin type positions: #42905.
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
=== NaNwill always returnfalseand!== NaNwill always returntrue.
Similarly,
Number.isNaNcould then be defined as:interface NumberConstructor { isNaN(number: unknown): number is NaN; }
There’s also real world code that relies on returning
NaNin acatchclause: 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.Reacted by Jeffrey Meyer and James Prevett19 remaining items
nicolas377 commented
on Jul 29, 2022 ContributorMore actionsSince 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
Number.isNaN()and the globalisNaN().I think this is pretty broken atm,
type Falsy = 0 | '' | false | null | undefined | -0 | 0n // | typeof NaN <-- doesn't work
typeof NaNresolves to type number but it is not a number,Also, to guard against truthy values, I had to create the
Falsytype alias (which doesn't work, cause NaN isn't literally typeable)export function assertFalsy(value: unknown): asserts value is Falsy { if (value) throw new AssertionError('Expected falsy value') }
Can't we just write
asserts not valueorasserts !value?Context of why this matters - Probably in new code you would add
null | undefinedto a type and then check for it understrict: true, then this won't matter, but I'm migrating a legacy code base that very loosely checks types and uses a lot of implicit conversions.muhamedkarajic commented
on Mar 29, 2023 More actionsIs this anywhere close to being fixed?
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.
Reacted by bbrk24, Miles B Huff, Theo Paris, 拾叁 and Escape0707I think this is unsolvable, as
a+ais 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:
- a type named
NotFraction, which would contain integers, infinities and NaNs – preserved by+,-,++,--,*,|,&,^,~, - a type named
NonnegativeNumber, which would contain nonnegative finite numbers (including -0) and positive infinity – preserved by+and++(in particular, not preserved by*,/or**, as0*Infinity,0/0and1**Infinityare NaN), - analogous
NonpositiveNumber(including +0), preserved by+and--, - a type named
PositiveNumber, which would contain positive finite numbers (excluding +0) and positive infinity – preserved by+,++and*(in particular, not preserved by**, as1**Infinityis NaN), - analogous
NegativeNumber(excluding -0), preserved by+and--, and withNegativeNumber * NegativeNumber = PositiveNumber, NonnegativeNotFraction = NotFraction & NonnegativeNumber, etc.- a type named
NaNcontaining NaN, just soFalsycould 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.
Reacted by Ricardo Fernández Serrata and Bence SzalaiReacted by Miles B Huff and Ricardo Fernández Serrata- a type named
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 << NaNis 0 and1 << Infinityis 1.Reacted by Miles B HuffI 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?
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
1instead of1.0even 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.0for 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, anywaysReacted by Jordan Harband and Michaël De BoeyHaving to use
1.0for floats is an idiom from some other programming languages that shouldn't be forced onto TypeScript users for no good reason.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.
Reacted by whzx5byb, ExE Boss, Miles B Huff and Ricardo Fernández SerrataBeing 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.
Reacted by Ricardo Fernández SerrataReacted by Ricardo Fernández SerrataThis 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.
Reacted by Ricardo Fernández Serrata and Escape0707As previously said, you cannot have an integer type that doesn't immediately dissolve into nonsense.
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 ++ and -- won't go into infinity.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
Reacted by ExE Boss and Ricardo Fernández SerratahavingPreconditionRefinement 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)
An
inttype or equivalent could also help avoidInfinityslipping into data destined for JSON serialization.
Related: #1897
I have read the FAQ and looked for duplicate issues.
Search Terms
Related Issues
Suggestion
NaNhas 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
NaNerrors, because you cannot add a number to an object, for example. But there are many math operations that can returnNaN. TheseNaNvalues 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 theNaNcame from.I would like TypeScript to provide a better way of preventing runtime
NaNerrors, by ensuring that an unhandledNaNvalue 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 codeerror could never be thrown, because the conditions appear to be exhaustive, and the types ofaandbarenumber. It is very easy to forget thatNaNbreaks all the rules of comparison and equality checks.It would be really helpful if TypeScript could warn about the possibility of
NaNwith a more fine-grained type system, so that the programmer was forced to handle these cases.Possible Solutions
TypeScript could add a
--strictNaNChecksoption. 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
realNumberis 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
--strictNaNChecksoption is disabled (default), then theintegerandfloattypes would also includeNaNandInfinity:I would personally be in favor of making this the default behavior, because
NaNerrors 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
NaNerrors. It would be really awesome if it did!(Sorry for the Flow example, but this is a real-world example where a
NaNtype 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(unlessais anumbertype that could potentially beNaN.)NaNis only possible for0 / 0. So if either the dividend or the divisor are non-zero numbers, then theNaNtype 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
Excludeconditional 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: