null か undefined か、どちらを使うかが問題だ

API を設計する、ドキュメント DB のデータ構造を定義する、JavaScript/TypeScript の内部的な型を定義する、というような場合、JSON で型設計を行うことになります。その際、避けて通れないのが、値が存在しないかも知れないプロパティの扱いに関する設計方針の検討です。Haskell についても少しだけ言及します。

以下写真は 0、`null`、`undefined` のとてもわかり易い概念です。

![0 vs null vs undefined](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31613/dc7e1c27-adca-28c4-c640-267b8cf7f766.jpeg)
[0 vs null vs undefined](https://twitter.com/ddprrt/status/1074955395528040448) より引用。

## TL;DR

- 実際は `null` でも `undefined` でもそれほど大差はない
- JavaScript/TypeScript では `== null`、`!= null` で `null` と `undefined` をまとめて判定したい
- Haskell では `null` で扱うのが楽
- どちらでも良いが、システム、アプリで `null` または `undefined` を統一する

## JSON

例えば会員情報として以下を扱う JSON の型定義をしてみます。

- ID
- 名前
- 年齢（オプション）

年齢がオプションなので、未設定の場合にどのような表現とするかを考えなければなりません。

**パターン 1: 年齢プロパティを省略**

```json
{
  "id": 42,
  "name": "Marty McFly"
}
```

**パターン 2: 年齢プロパティに `null` 指定**

```json
{
  "id": 43,
  "name": "Biff Tannen",
  "age": null
}
```

**パターン 3: 年齢プロパティに `undefined` 指定**

```json
{
  "id": 44,
  "name": "Emmett Brown",
  "age": undefined
}
```

このうちパターン 3 は、JSON の値として認められていない `undefined` を使用しているため、JSON としてはありえません。つまり JSON の表現としてはパターン 1 のプロパティを省略することで値が存在しないことを表すか、パターン 2 の `null` で値が存在しないことを表現するかのいずれかになります。

## JavaScript

JSON を CSV に変換する JavaScript コードを書いてみます。

```csv.js
const users = [
  {
    "id": 42,
    "name": "Marty McFly"
  },
  {
    "id": 43,
    "name": "Biff Tannen",
    "age": null
  }
]
const csv = users.reduce((acc, cur) => {
  const record = Object.keys(cur).map(key => JSON.stringify(cur[key])).join(',')
  return `${acc}${record}\n`
}, `${Object.keys(users[0]).join(',')}\n`)

console.log(csv)
```

このコードの実行結果は以下のとおりです。

```shell-session
$ node csv.js
id,name
42,"Marty McFly"
43,"Biff Tannen",null
$
```

出力した CSV に必要なヘッダが含まれていません。これはリストのオブジェクトの形式が一致していないために起こる不具合です。JavaScript は型に関してプログラミング言語としてプログラマを何もサポートしてくれないためしばしばこうしたことが起こり得ます。気をつけるしかありません。

## TypeScript

TypeScript なら型を付けられるので、JavaScript のような不幸は発生しません。

**パターン 1: 年齢プロパティを省略可能**

```ts
interface User {
  id: number
  name: string
  age?: number
}
```

**パターン 2: 年齢プロパティに `null` 許容**

```ts
interface User {
  id: number
  name: string
  age: number | null
}
```

**パターン 3: 年齢プロパティに `undefined` 許容**

```ts
interface User {
  id: number
  name: string
  age: number | undefined
}
```

パターン 1 とパターン 3 は似ているようで違います。パターン 1 はプロパティを省略可能ですが、パターン 3 はプロパティを明示し `undefined` を指定しなければなりません。

また、パターン 2 とパターン 3 も異なります。オブジェクトを JSON に変換した際に、値が `undefined` の場合はプロパティが省略されます。

```js
> JSON.stringify({
  "id": 42,
  "name": "Marty McFly",
  "age": null
})
'{"id":42,"name":"Marty McFly","age":null}'
> JSON.stringify({
  "id": 42,
  "name": "Marty McFly",
  "age": undefined
})
'{"id":42,"name":"Marty McFly"}'
>
```

## OpenAPI

API 設計の観点ではどうでしょうか。OpenAPI 3.0 では以下のような感じになります。

**パターン 1: 年齢プロパティを省略可能**

```yaml
components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        age:
          type: integer
          format: int32
      required:
        - id
        - name
```

**パターン 2: 年齢プロパティに `null` 許容**

```yaml
components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        age:
          type: integer
          format: int32
          nullable: true
      required:
        - id
        - name
        - age
```

Swagger UI での見た目は以下のようになります。

**パターン 1: 年齢プロパティを省略可能**
![パターン 1: 年齢プロパティを省略可能](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31613/d87de2c8-c58d-97c1-0a25-3a2b6b0bcd9c.png)

**パターン 2: 年齢プロパティに `null` 許容**
![パターン 2: 年齢プロパティに `null` 許容](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31613/46ad20a6-924b-928c-59fa-ab3e2e2b8734.png)

OpenAPI では `nullable` という指定ができるようになったのは 3.0 になってからで、それまでは API 仕様として `null` を表すことができなかった都合上、パターン 1 がよく使われている印象です。ただし、API 仕様書がない外部 API の返戻値のプロパティが省略されておらず `null` を値として持っていると助かることが多いです。

## ドキュメント DB

ドキュメント DB でプロパティを領略する場合と値として `null` を許容する場合ではクエリの書き方が変わってきます。ここでは Microsoft Azure の Cosmos DB を例にしてみます。

**パターン 1: 年齢プロパティを省略可能**

```sql
SELECT * FROM c WHERE IS_DEFINED(c.age);
SELECT * FROM c WHERE NOT IS_DEFINED(c.age);
```

**パターン 2: 年齢プロパティに `null` 許容**

```sql
SELECT * FROM c WHERE c.age != null;
SELECT * FROM c WHERE c.age = null;
```

クエリ自体はパターン 1 に Cosmos DB の方言が含まれてはいるものの、どちらがどうである、ということはなさそうです。ただし、このドキュメントの持ち方が API 仕様にも影響を与え、バックエンド、そしてフロントエンドの実装にも影響を与えることになるため、慎重に設計する必要はあります。

## JavaScript/TypeScript による判定方法

JavaScript/TypeScript では、プロパティが定義されておらず `undefined` なのか、値が `null` なのかを判別する良い方法があります。

```js
const users = [
  {
    "id": 42,
    "name": "Marty McFly"
  },
  {
    "id": 43,
    "name": "Biff Tannen",
    "age": null
  }
]
users.forEach(user => {
  if (user.age != null) { // ← このように判定する
    console.log('Age is:', user.age)
  }
})
```

`undefined` や `null` でないことを判定するには `if (user.age) {` でもできますが、これだと年齢が ０ 歳の場合に正しく動作しません。厳密に行うと `if (user.age !== undefined && user.age !== null) {` となりますが、これがまさに `if (user.age != null) {` と表現できるため個人的にこの記法を推奨しています。プロパティがなくて `undefined` なのか、値を `null` として値がないことを表現するのかにとらわれずに一貫した書き方ができます。ESLint でも `==` や `!=` を使用すると警告が出ますが、`null` と比較する場合に限り警告は出ません。JavaScript/TypeScript の言語仕様では `null` と `undefined` は明確に違いますが、ビジネスロジックで `null` と `undefined` に異なる意味づけをしないようにすべきと強く推奨します。

## Haskell

唐突に Haskell の話をします。Haskell では存在するかどうかわからない値は `Maybe` を用いて表現します。JSON と Haskell のデータ型を相互変換するライブラリとして最も使われているのは aeson でしょうから、以下に aeson だとどのようになるかを紹介します。

```haskell
data User = User
  { id :: Integer
  , name :: String
  , age :: Maybe Int
  } deriving (Show, Generic)
```

JSON に変換可能なデータの定義方法はいくつかあるのですが、今回は `Generic` で自動導出することにします。REPL で実行すると以下のことがわかります。

- Haskell のデータ型では `Nothing` を明示する必要がある
- aeson で JSON にエンコードすると `Nothing` は `null` になる
- プロパティが省略されている JSON を aeson でデコードすると `Nothing` になる
- aeson ではパターン 1 を読み込み可能としつつ、パターン 2 に統一する方向で動作する

```haskell
> let user = User {id = 42, name = "Marty McFly", age = Nothing}
> user
User {id = 42, name = "Marty McFly", age = Nothing}
> encode user
"{\"age\":null,\"name\":\"Marty McFly\",\"id\":42}"
> decode "{\"id\":42,\"name\":\"Marty McFly\"}" :: Maybe User
Just (User {id = 42, name = "Marty McFly", age = Nothing})
>
```

## まとめ

オブジェクト、JSON、API、ドキュメントで存在しない値を表現するためにプロパティを省略するか `null` を許容するかについては、若干の一長一短はあるものの（美意識の差とか、慣習とか、多少のデータ量とか、多少のタイプ量とか）、どちらでも良いかと思います。ただし、システム内やアプリ内で統一すること、値が存在しないことをコードで適切に判定することが大切です。そのために静的型付けが利用できれば更に良いです。