TypeScriptのコーディングでnullとundefinedを使い分けるべきか?

個人的な感想です。

結論

  • 使い分ける派
  • 寄せる派
    • undefined寄せ派 (← 個人的にはここ)
    • null寄せ派

議論

客観的にガイドラインを決めるのが案外難しい

一般的にnullundefinedの違いは下記の様に整理されており、 一見すると両者は厳密に使い分けできる様に思えます。

  • null =>「存在しない」
  • undefined => 「定義されていない」

では、反例がないか確認していきたいと思います。

まず、下記の様な場合を検討してみます。

  1. document.querySelector(".存在しないセレクター")の戻り値は null
  2. 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を代入した場合はnullundefinedのどちらを返すのが正しいでしょうか? 関数の意図としては、「全てのAタグの中から1_000_000_000番目登場するAタグを取得する」という問題になっているので、そんなAタグがない場合は、 定義されていないというより、存在しない(=null)を返すのが正しのではないでしょうか?

ここで矛盾が生じました。
2では、undefinedを返すのが正しいという結論になりましたが、関数に切り出した場合はnullを返すのが正しいという結論になりました。


では、なぜこの様な違いが出るかを考えると、2では手続きに着目したのに対して、関数に切り出した場合は意味に着目したからです。
ちなみに、ここまでの議論にすでに反論がある方も多くいると思います。


この様に、客観的にnullundefinedを使い分けるのは案外難しく、見方によって変わってしまうのです。

さらに根本論でいうと、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はどうしても自然発生する
  • undefinedへの統一の難しさ
    • 外部システムがnullを返す場合がある
      • DB, API, etc...
  • 中立
    • JSON.stringifyの結果に残る

個人的な感想

論理学(私はわからない)的に厳密性を突き詰めることができるかもしれませんが、 案外とプログラマーは論理的な厳密性よりも直感的であることを好む傾向があると思っています。
そのため、なるべくundefinedに統一し、認知的なコストを減少させるたほうが価値があると考えています。

余談

話は変わりますが、例えばこちらのツイートに関して、表現はさておき内容には関しては同意しています。

つまり、allItemIsNum([])の様な関数に空配列を渡した戻り値がtruefalseどちらがよいか?という議論ですが、 この場合は、多くの経験のある(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で議論されていますが、 そうでなければnullundefined派はおろか、throw派が現れても不思議はありません。

この様に、登場人物を増やすということが、こう言った境界値の議論をより難しくすることにもなりますので、 特別な理由がなければ選択肢を減らしていくのが効率的かな考えています。

参考

サバイバルTypeScript

もしどちらを使うべきか迷ったらundefinedを使っておくほうが無難です。 特にこだわりがないのなら、TypeScriptではnullは使わずにundefinedをもっぱら使うようにするのがお勧めです。

TypeScriptのレポ

Use undefined. Do not use null.

※ ただし、これがTSの正しいガイドを意図してない旨excuseあり。