🕌

TypeScriptではNumber.isNaN()よりもisNaN()の方が安全かもしれない

2023/08/01に公開

これまで「グローバルのisNaN()ではなくNumber.isNaN()を使え!」を教義に生きてきたのですが、揺らいできました。

JavaScriptのisNaN()は引数を数値に変換した結果がNaNであるかを判定します。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isNaN

一方のNumber.isNaN()はES6で提供された関数で、引数がNaN以外の時はtrueを返しません。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN

console.log(isNaN('hello'));  // true
console.log(Number.isNaN('hello'));  // false

Number.isNaN()はより厳密な比較であり、キャストを行わないため余計な混乱を避けられるものとして提供されました。

文字列などを渡した場合はtrueを返さないため、Number.isNaN()に渡す時点であらかじめ数値に変換しておくなど型を意識した操作がアプリケーション側の責務として求められるようになりました。

これは正当進化であると捉えES6以降ずっとNumber.isNaN()を使っていたのですが、TypeScriptでは事情が異なるかもしれません。

TypeScriptの型はグローバルなisNaNの方が堅い

現在のTypeScriptの型定義ではグローバルなisNaN()の引数はnumberに限定されており、

https://github.com/microsoft/TypeScript/blob/d9e327b3635120fba9593b54d6b20442bc8f40f5/src/lib/es5.d.ts#L36

Number.isNaN()は引数をunknownと見るように定義されています。

https://github.com/microsoft/TypeScript/blob/d9e327b3635120fba9593b54d6b20442bc8f40f5/src/lib/es2015.core.d.ts#L221

GitHubを見ると「なんでisNaN()が数値しか受け付けへんねん」系のissueはわんさか出てきますが、最初期のissueでもう結論は出ているようです。2015年なのでだいぶ前ですね。

https://github.com/microsoft/TypeScript/pull/3947
https://github.com/microsoft/TypeScript/issues/4002

挙動の複雑さが混乱を招くこと、入り口で型を絞ることでそれを防ぐことをTypeScriptの役割として重視している様子が伺えます。

Number.isNaN()は当初はnumberしか受け付けなかったものが、グローバルなisNaNに比べて混乱を招く恐れが低いことを重視してかanyを受けるように変更されています。

https://github.com/microsoft/TypeScript/pull/24436/

この変更は2018年なので、ES6が普及しだした頃は「どちらもnumberしか受け付けないんだからどうせならNumber.isNaN()を使っておけ」と判断しても妥当だったと言えそうです。当時の自分がそこまで見ていたかは記憶にありません…

変更履歴を追っていくと、Number.isNaN()の引数は後にnumberに戻された後で現在はunknownに落ち着いています。どうもnumberに戻ったのは意図的ではなかったっぽい?

https://github.com/microsoft/TypeScript/issues/34931
https://github.com/microsoft/TypeScript/pull/34932

そんなこんなを経て、runtimeの挙動はともかくTypeScriptで書く上ではグローバルなisNaN()の方が型が限定されるという側面で堅く書ける状況になっています。

ここ数年Number.isNaN()で判定していたのですが、現状を見るとグローバルなisNaN()に切り替えようかなぁと考えています。

ESLintはこのへんをどう見ているか

本件に気付いたのは「Number.isNaN()の使用を強制するESLintのルールが見当たらないけど、それぐらいありそうじゃない?」と思ったのがきっかけでした(改めて挙動を確認してたら両者の引数の型定義が違った)。

で、ESLintはどうかと言うと2013年にissueは立ったものの、

https://github.com/eslint/eslint/issues/375

当時はES6は一般に広く使われておらずpolyfillの導入が必要だったため、それを強制するのは現実的ではないと判断されています。しゃーない。

ところが、後の世になると

https://github.com/eslint/eslint/issues/10313

no-restricted-globalsで定義できるし、これだけのためのルールは追加したくないとか

https://github.com/eslint/eslint/issues/14545

悪いなのび太、新規ルールは最近stage4に上がった機能しか取り扱わないんだ、みたいな扱いをされてしまっています。歴史の狭間に埋もれてしまった感が否めない…

個人的にはrecommendedに入るかどうかが検討されてもいいんじゃないのかと思ったので、この塩対応は悲しいのですがTypeScript側で守れるならまぁよしとするかで着地しました。

isNaN()を禁止するなら

"no-restricted-globals": ["error", { name: "isNaN" }],

逆にNumber.isNaN()を禁止するには

"no-restricted-properties": ["error", { object: "Number", property: "isNaN" }],

を設定することになりますが、わざわざ書くかと言うと個別ルールは増やしたくないのでそこまではいいかなぁ。

Discussion