「JSONとの間で変換/復元が可能な値」を表すTypeScript向けの小さな型定義

アプリケーションを書いていると、オブジェクトのJSONへの変換や、JSONからのオブジェクトの復元を行うことはよくあります。

ですが、TypeScript上でこうした処理を頻繁に行っていると、「あるオブジェクトや変数の値が、**JSONとの間で確実に（一部の情報を失ったりせずに）変換/復元が可能なこと**を型レベルで保証したい！」と思うことはよくあります。ありますよね？

```ts
let food1 = {
    name: 'スクランブルエッグ'
    , calorie: 230
    , stuff: ['卵', '油', '塩']
}

console.log(food1);
// => { name: 'スクランブルエッグ', calorie: 230, stuff: [ '卵', '油', '塩' ] }
console.log(JSON.parse(JSON.stringify(food1)));
// => { name: 'スクランブルエッグ', calorie: 230, stuff: [ '卵', '油', '塩' ] }
//   JSONに変換した値を、完全に復元できている

let food2 = {
    name: 'スクランブルエッグ'
    , calorie: 230
    , stuff: ['卵', '油', '塩']
    , created: new Date()
    , updated: undefined
}

console.log(food2);
// => { name: 'スクランブルエッグ', calorie: 230, stuff: ['卵', '油', '塩'], created: 2019-01-19T14:41:04.353Z, updated: undefined }
console.log(JSON.parse(JSON.stringify(food2)));
// => { name: 'スクランブルエッグ', calorie: 230, stuff: ['卵', '油', '塩'], created: '2019-01-19T14:41:04.353Z' }
//   Date型はJSONに変換してしまうと、追加処理なしでは正しく復元できない！
//   また、undefinedもJSONに変換した時点で消滅してしまうため、正しく復元できない！
```


そこで、「JSONとの間で変換/復元が可能な値」を表す**JSONSerializable**型の定義を作りました。
たった数行の短い型定義なので、特にnpmパッケージなども作っていませんが、有用に思われた方はコピー＆ペーストしてご利用ください。
（配列の型定義方法は、[GithubにあるTypeScriptプロジェクトのissues](https://github.com/Microsoft/TypeScript/issues/3496#issuecomment-128553540)を参考にさせていただきました）

```ts:JSONSerializable.d.ts
/** JSONとの間で、相互に変換/復元が可能な値 */
type JSONSerializable = JSONSerializablePrimitive | JSONSerializableArray | JSONSerializableObject;

type JSONSerializablePrimitive = null | boolean | string | number;

type JSONSerializableObject = { [key: string]: JSONSerializable };

interface JSONSerializableArray extends Array<JSONSerializable> {
}
```

さっきのサンプルコードに型を適用すると、このように適切にビルドエラーを出力してくれます。

```ts
let food1: JSONSerializable = {
    name: 'スクランブルエッグ'
    , calorie: 230
    , stuff: ['卵', '油', '塩']
}
// => エラーなし

let food2: JSONSerializable = {
    name: 'スクランブルエッグ'
    , calorie: 230
    , stuff: ['卵', '油', '塩']
    , created: new Date()
    , updated: undefined
}
// => ビルドエラーが発生する 
//   型 '{ name: string; calorie: number; stuff: string[]; created: Date; updated: undefined; }' を
//   型 'JSONSerializable' に割り当てることはできません。（以下略）


// 「変換可能なobjectであること（文字列/数値/真偽値/null/配列ではないこと）」を強制したい場合は
// JSONSerializableObject型を使用する
let food3Obj: JSONSerializableObject;
food3Obj = {name: 'たまごやき'};  // OK
food3Obj = 'たまごやき';          // ビルドエラー (object型ではない)
```

ただし、この型定義の制限として、**循環オブジェクトはエラーとして弾けない**ことにご注意ください。
（もし循環オブジェクトも弾ける方法があれば教えてください:pray_tone1:）

```ts
let node1 = {
    parent: null
}
let node2 = {
    child: null
}
node1.parent = node2;
node2.child = node1;

let n1: JSONSerializable = node1; // エラーなし
let n2: JSONSerializable = node2; // エラーなし

console.log(JSON.stringify(n1)); // 循環しているためエラー！ (TypeError: Converting circular structure to JSON)
```

(2020/8/1追記)
なお、場合によっては「オブジェクトのプロパティがundefined値を持つことだけは許可したい」という場合もあると思います。（JSONからの変換/復元を介した時に、値がundefinedであるプロパティが削除されていても問題ない場合）
その場合は、代わりに下記のような定義を使用してください。

```ts:JSONSerializable.d.ts
/** JSONとの間で、相互に変換/復元が可能な値 */
type JSONSerializable = JSONSerializablePrimitive | JSONSerializableArray | JSONSerializableObject;

type JSONSerializablePrimitive = null | boolean | string | number;

type JSONSerializableObject = Partial<{ [key: string]: JSONSerializable }>;

interface JSONSerializableArray extends Array<JSONSerializable> {
}
```