microsoft / microsoft/TypeScript
Add a --strictNaNChecks option, and a NaN / integer / float type to avoid runtime NaN errors
まだ誰も着手していません。
- 主要言語
- Go
- スター
- 111k
- フォーク
- 14.3k
- 平均マージ
- 1日 19時間
- マージ済み PR(30日)
- 117
説明
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.
コントリビューションガイド
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
調査の方向性
まず strictNullChecks、NaN と Infinity のリテラル型、整数型、BigInt に関する関連 issue を読み、その後、チェックリストにリンクされている TypeScript Design Goals を確認します。提案された strictNaNChecks の動作、数値型、narrowing ルール、互換性への影響について、合意された設計と対応する実装計画ができた時点で作業は完了です。
索引モデルが issue の本文から書いたものです。
評価
- 技術スタック
- javascript, typescript
- 領域
- compilers
- issue の種類
- 機能追加
- 難易度
- 5/5
- 見積もり時間
- 1週間以上
- 活発さ
- 停滞
- 明瞭さ
- おおむね明確
- 初心者へのやさしさ
- 25/100