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


  [自ブログ](https://blog.hedrall.work/posts/ts-null-coding)からの引用です。
  
  個人的な感想です。

# 結論

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

# 議論

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

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

- `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`を返しました。
これはいいと思います。<br />
一方で、2では考え方が二段階になっており、まず `document.querySelectorAll("a")` で配列を定義してから、その配列に定義されていないIndexを指定したことで`undefined`が返されています。
これも良いですね！ここまでは違和感なく使い分けができています。

では、続いて2のケースを関数に切り出してみます。

```typescript
const getATagElementAt = (index: number) => document.querySelectorAll("a")[index]; // `???`
```

この関数に`1_000_000_000`を代入した場合は`null`と`undefined`のどちらを返すのが正しいでしょうか？
関数の意図としては、「全てのAタグの中から`1_000_000_000`番目登場するAタグを取得する」という問題になっているので、そんなAタグがない場合は、
定義されていないというより、存在しない(=`null`)を返すのが正しのではないでしょうか？

ここで矛盾が生じました。<br/>
2では、`undefined`を返すのが正しいという結論になりましたが、関数に切り出した場合は`null`を返すのが正しいという結論になりました。
<br/>
では、なぜこの様な違いが出るかを考えると、2では**手続き**に着目したのに対して、関数に切り出した場合は**意味**に着目したからです。<br/>
ちなみに、ここまでの議論にすでに反論がある方も多くいると思います。
<br/>
この様に、客観的に`null`と`undefined`を使い分けるのは案外難しく、見方によって変わってしまうのです。

さらに根本論でいうと、1. `document.querySelector(".存在しないセレクター")`は本当に`null`が正しかったのでしょうか？
ブラウザを世界そのものと考えれば、そこに要素が「存在しない」ということになりそうですが、
そもそもHTMLを書いたページの製作者がいるはずで、その方からすれば「定義していない要素」ということにならないでしょうか？

この場合の矛盾の要因は、主体が**存在をコントロールできる側**と**できない側**で`null`, `undefined`が変わることを意味します。

## 論理値の分類問題

さらに細かい話をすると、

```typescript
// Aタグが9個しかないとして
const result10 = getATagElementAt(10); // => null
const resultInf = getATagElementAt(Infinity); // => null ???
```

この両者の結果は厳密には異なる可能性があります。
まず、`result10`は10番目の要素が存在しないので、言い換えると「10番目のAタグは？」という問題に対する「**不成立の条件**」と言えます。<br/>
一方、`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の結果に残る

# 個人的な感想

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

# 余談

話は変わりますが、例えば[こちらのツイート](https://twitter.com/fumieval/status/1663161595009314819)に関して、表現はさておき内容には関しては同意しています。

つまり、`allItemIsNum([])`の様な関数に空配列を渡した戻り値が`true`、`false`どちらがよいか？という議論ですが、
この場合は、多くの経験のある(JS)エンジニアが連続性(下図)を意識した `ture` を選択するのではないでしょうか？ <br/>
しかし、空配列に関する動作はそもそも定義されないと考えれば`false`も十分ありえます。

```typescript
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`派が現れても不思議はありません。

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

# 参考

[サバイバルTypeScript](https://typescriptbook.jp/reference/values-types-variables/undefined-vs-null)

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

[TypeScriptのレポ](https://github.com/Microsoft/TypeScript/wiki/Coding-guidelines#null-and-undefined)

> Use undefined. Do not use null.

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

- 記事
  - [JavaScript の undefined と null を完全に理解する](http://nmi.jp/2022-10-17-Understanding-Undefined-And-Null)