TypeScriptのコーディングでnullとundefinedを使い分けるべきか?
自ブログからの引用です。
個人的な感想です。
結論
- 使い分ける派
- 寄せる派
- undefined寄せ派 (← 個人的にはここ)
- null寄せ派
議論
客観的にガイドラインを決めるのが案外難しい
一般的にnull
とundefined
の違いは下記の様に整理されており、
一見すると両者は厳密に使い分けできる様に思えます。
-
null
=>「存在しない」 -
undefined
=> 「定義されていない」
では、反例がないか確認していきたいと思います。
まず、下記の様な場合を検討してみます。
-
document.querySelector(".存在しないセレクター")
の戻り値はnull
-
document.querySelectorAll("a")[1_000_100_000]
の戻り値はundefined
(1_000_100_000は存在しないIndexを意図しています。)
1でdocument.querySelector
は、ブラウザのwindow
という系
に存在しないセレクターを検索した結果、存在しないことを示すnull
を返しました。
これはいいと思います。
一方で、2では考え方が二段階になっており、まず document.querySelectorAll("a")
で配列を定義してから、その配列に定義されていないIndexを指定したことでundefined
が返されています。
これも良いですね!ここまでは違和感なく使い分けができています。
では、続いて2のケースを関数に切り出してみます。
const getATagElementAt = (index: number) => document.querySelectorAll("a")[index]; // `???`
この関数に1_000_000_000
を代入した場合はnull
とundefined
のどちらを返すのが正しいでしょうか?
関数の意図としては、「全てのAタグの中から1_000_000_000
番目登場するAタグを取得する」という問題になっているので、そんなAタグがない場合は、
定義されていないというより、存在しない(=null
)を返すのが正しのではないでしょうか?
ここで矛盾が生じました。
2では、undefined
を返すのが正しいという結論になりましたが、関数に切り出した場合はnull
を返すのが正しいという結論になりました。
では、なぜこの様な違いが出るかを考えると、2では手続きに着目したのに対して、関数に切り出した場合は意味に着目したからです。
ちなみに、ここまでの議論にすでに反論がある方も多くいると思います。
この様に、客観的にnull
とundefined
を使い分けるのは案外難しく、見方によって変わってしまうのです。
さらに根本論でいうと、1. document.querySelector(".存在しないセレクター")
は本当にnull
が正しかったのでしょうか?
ブラウザを世界そのものと考えれば、そこに要素が「存在しない」ということになりそうですが、
そもそもHTMLを書いたページの製作者がいるはずで、その方からすれば「定義していない要素」ということにならないでしょうか?
この場合の矛盾の要因は、主体が存在をコントロールできる側とできない側でnull
, undefined
が変わることを意味します。
論理値の分類問題
さらに細かい話をすると、
// Aタグが9個しかないとして
const result10 = getATagElementAt(10); // => null
const resultInf = getATagElementAt(Infinity); // => null ???
この両者の結果は厳密には異なる可能性があります。
まず、result10
は10番目の要素が存在しないので、言い換えると「10番目のAタグは?」という問題に対する「不成立の条件」と言えます。
一方、resultInf
の方はどうでしょうか?「無限番目のAタグ」というのは立場によりますが、「成立しえない条件」となります。
この場合は、存在するしないの問題ではなく、問題自体が成立してないので一概にnull
を返すのが正しいとも言い切れません。
「そんなものは想定していないですよ〜」ということであれば、undefined
が正しい様にも思えます。
この議論はRDBMSの3論理値を参考にしていますが、一重に論理的な否定の中にも複数の種別が存在しえます。
事実の整理
- Null自体の問題点
-
typeof null
=>object
- リテラルなのに暗黙的
-
Number(null)
=>0
,Number(undefined)
=>NaN
-
- Nullへの統一の難しさ
- TypeScriptにおいてオプショナル
?
を許しづらくなる -
undefined
はどうしても自然発生する
- TypeScriptにおいてオプショナル
- undefinedへの統一の難しさ
- 外部システムが
null
を返す場合がある- DB, API, etc...
- 外部システムが
- 中立
- JSON.stringifyの結果に残る
個人的な感想
論理学(私はわからない)的に厳密性を突き詰めることができるかもしれませんが、
案外とプログラマーは論理的な厳密性よりも直感的であることを好む傾向があると思っています。
そのため、なるべくundefined
に統一し、認知的なコストを減少させるたほうが価値があると考えています。
余談
話は変わりますが、例えばこちらのツイートに関して、表現はさておき内容には関しては同意しています。
つまり、allItemIsNum([])
の様な関数に空配列を渡した戻り値がtrue
、false
どちらがよいか?という議論ですが、
この場合は、多くの経験のある(JS)エンジニアが連続性(下図)を意識した ture
を選択するのではないでしょうか?
しかし、空配列に関する動作はそもそも定義されないと考えればfalse
も十分ありえます。
const result_1 = allItemIsNum(['1', '2', '3']); // => true
const result_2 = allItemIsNum(['1', '2']); // => true
const result_3 = allItemIsNum(['1']); // => true
const result_4 = allItemIsNum([]); // => true
ここで注目したいのは、こう言った場合の議論が難しいことです。
なぜなら、下記の様に異なる観点がぶつかってしまうからです。
- 論理的な厳密さ
- 戻り値の予測しやすさ
- 関数の振る舞いとしての正しさ
- 実用性
この場合は、boolean
に閉じているので、true
or false
で議論されていますが、
そうでなければnull
やundefined
派はおろか、throw
派が現れても不思議はありません。
この様に、登場人物を増やすということが、こう言った境界値の議論をより難しくすることにもなりますので、
特別な理由がなければ選択肢を減らしていくのが効率的かな考えています。
参考
もしどちらを使うべきか迷ったらundefinedを使っておくほうが無難です。
特にこだわりがないのなら、TypeScriptではnullは使わずにundefinedをもっぱら使うようにするのがお勧めです。
Use undefined. Do not use null.
※ ただし、これがTSの正しいガイドを意図してない旨excuseあり。
Discussion