JavaScriptでtypeof x === 'undefined'を使わないで欲しい理由
何故嫌なのか説明します.
結論追記
アクセス数がそこそこあるので短く結論.
基本的にx == null
を使ってください.
ESLintのだいたいのルールでも==
の曖昧比較はx == null
だけは許可しているはずです.
nullを弾けない
> typeof null
'object'
となります.
undefinedの場合弾いて, nullの場合許容したい時があるでしょうか. 私はほとんどそんなものは見たことがありませんし, それはしばしば設計ミスを示しています. 一般的にnullも同じく弾くべきです.
変数のフィールドを参照した際のTypeErrorはundefinedでもnullでも起きるため, 両者を弾く意義は大いにあります.
falseを弾けない
> typeof false
'boolean'
undefinedとnullを弾いてfalseを許容するプログラムは実際見たことがありますが, これもややこしく設計ミスの可能性が高いです.
意図した場合だとしても,
false以外を弾くならx === false
とした方が意図が明白です.
空文字列を弾けない(賛否両論)
> typeof ''
'string'
undefinedとnullを許容しないで空文字列を許容するプログラムは割と見ますし,
設計ミスではないこともよくあります.
Array#join
とかですね.
いや,
Array#join
に引数指定しないとセパレータが','
になるのは設計ミスか?
しばしば悩まされていますし.
まあそれはともかく, 空文字列を弾きたいという時は結構あるのでここは賛否両論ですね.
後述して複数の選択肢を提示します.
タイプミスを疑ってしまう
> u = undefined
undefined
> typeof u === 'undefinde'
false
バグ調査の時こういうつまらないミスがないか確認するのが手間. ですが, ESLintはこれ警告してくれるのでこれはあまり理由にならないかもしれません.
素直にBooleanに変換した方が短い(1つめの選択肢)
if (typeof x === 'undefined')
より
if (x)
と書いたほうが読む側にとっても楽です.
しかしこれにも問題はあります.
JavaScriptでは
- 0
- -0
- null
- false
- NaN
- undefined
- ''
はfalseに変換されます.
値が省略された場合や、値が 0, -0, null, false, NaN, undefined あるいは空文字列 ("") であった場合、オブジェクトは false の初期値を持ちます。
以下問題とその解決策を書いていきます.
問題が許容できない人は他の書き方の方が良いでしょう.
0がfalseになる問題
> Boolean(0)
false
0はfalseとして変換されます.
undefinedかnull以外の数値もしくはオブジェクトを期待しているときは以下のように書きましょう.
if (Number.isInteger(x) || x)
整数のみを期待している時は以下の方が違う値も弾けてなおかつシンプルで良いですね.
if (Number.isInteger(x))
浮動小数点数も受け入れたい場合はNumber.isFinite
で.
> Number.isFinite(0.1)
true
この場合NaNも弾かれますがundefinedがダメでNaNが良い時が存在するのか怪しい.
> Number.isInteger(NaN)
false
存在したとしてその時だけNumber.isNaN
をorに入れて許可すれば良いのではないでしょうか.
''がfalseになる問題
> Boolean('')
false
私はundefinedは非許容で, 空文字列は許容するのはややこしいと考えています.
空文字列を無入力として許容するならば,
デフォルト引数やx || ''
でフォールバック値に空文字列を指定しておけば良いと思います.
しかし, 空文字列は許容したいという時もそこそこあることは把握しています.
undefinedのみを弾きたい時もtypeofは不要(2つめの選択肢)
引数の省略を検知したい時などにundefinedのみを検知したい時はあるかもしれません.
そういう時でも以下のように書いたほうが簡潔です.
if (x === undefined)
undefinedは書き換えられられる可能性があるという情報も多いですが, もはや古代であるES5から書き換え不能なので気にしなくて良いです.
このご時世にローカルスコープのundefinedをシャドウするのは明らかに意図があるので, その場合はむしろ参照したほうが良いのではないでしょうか. そんな事例があるかはともかく.
if (x)
を使いたくない時でも面倒なtypeofは使わないで素直にx === undefined
を使いましょう.
Booleanへの変換を使ってJavaScriptの暗黙型変換に惑わされたくない時(3つめの選択肢)
暗黙型変換に付き合いたくない気持ちは確かにわかります.
実際空文字列はfalseで, 空オブジェクトはtrueとかややこしいですよね.
でもそういう時も
if (x === undefined || x === null)
と書いてnullは排除しましょう.
いずれにしろtypeofは不要です.
Booleanへの変換で惑わされたくないけど短い方が良い(4つめの選択肢)
x == null
を使いましょう.
==
での変換をすればAbstract Equality Comparison Algorithmでundefinedもnullになります.
多くのコードリンターは===
を推奨している時もx == null
のみ特例で許可します.
とにかくundefinedを弾く時はnullも弾いて欲しい
前述した通りプロパティが例外になるのは同じなので. 他は好みで良いです.
でもtypeofは変数が宣言されていない時もエラーを出さないから便利では?
挙動は確かにそうですね.
しかし, MDNに書かれている通り, JavaScriptは字句的スコープの言語です.
いずれにしても、この種のテクニックは避けるべきです。JavaScript は静的スコープの言語なので、変数が宣言されているか知りたければ、囲んでいるコンテキスト内でその変数が宣言されているか見ればいいのです。唯一の例外はグローバルスコープですが、グローバルスコープはグローバルオブジェクトに束縛されれているため、グローバルなコンテキスト内に変数が存在するか確認したければ、グローバルオブジェクト上のプロパティの存在を確認する (例えば in 演算子を使って) ことで行えます。
モジュール化されたJavaScriptではグローバルスコープも含めて定義されていない変数はESLintで静的検証できます.
require
もimport
も使えない環境で開発してる?
大変そうですね.
そういう時,
MDNではin
を使ったグローバルスコープのルックアップを参照を推奨していますが,
undefinedやnull値が入ったプロパティを弾けないので私は推奨しません.
> window = {a: null, b: undefined}
{ a: null, b: undefined }
> 'a' in window
true
> 'b' in window
true
> 'c' in window
false
素直にwindowを参照すれば良いでしょう.
> window.a
null
> window.b
undefined
> window.c
undefined
非ブラウザ環境にはwindowは無いでしょうが, Nodeはそもそもrequireがありますし, 大概の環境でwindowに相当するグローバルオブジェクトがあるでしょう. Web Workerなら(WorkerGlobalScope.self)とか. ない場合(ないことがあるのか?)はそもそもグローバル変数作れないので気にする必要なし.
ユニバーサルJavaScriptにしたい? モジュール機構なしにユニバーサルJavaScriptで書くのが無理がありそうです.
typeofでもネストしたフィールドを参照するとTypeErrorになるので混乱の元になる
やっと本題です.
> table = {user: 'ncaq', friends: null}
{ user: 'ncaq', friends: null }
> typeof table.friends[0]
TypeError: Cannot read property '0' of null
typeofはネストしたプロパティを辿ると普通に例外を出します.
1段目のプロパティアクセスは無問題という油断がバグを生み出しやすいと考えています. 今回記事を書いた原因です.
typeofには頼れないので1つ1つnullケアしていきましょう.
nullチェックがつらい
JavaScriptでエラーを返さずにネストした値のnullチェックを行う良い標準的な方法が存在しない.
Lodashのgetのようなものでも, 文字列渡しでチェックしていてスマートじゃないですね.
JavaScriptにはマクロがないのでどうしてもこうなってしまう.
tc39/proposal-optional-chainingが入ればチェックも少しは楽になるのですが.
null排除にはTypeScriptかFlowかReasonかPureScriptかElmとかを使いたい. JavaScriptつらい.