JSONをTypeScriptの型でバリデーションしたい！

## やりたいこと
JSONのTypeScriptの型でバリデーションしたいです。定義するコストは最小限にとどめたいです。

具体的には、
文字列になった`'{"name": "jack", "age": 8}'`などが`type Person = {name: string, age: number}`をちゃんと満たしているかを確かめたいです。確かめたあとは、`Person`型として扱いたいです。
JSONはネットワークから飛んできたりして、それがTypeScriptの型システムの中で安全に使えるか確かめる仕組みがほしいです。


一番この記事で伝えたのは、**JSONの構造を一度定義したら、TypeScriptの型を導出する**仕組みです。いろんなTypeScriptのバリデーションライブラリ調べてみましたが、型を自動で導出しているものは見つけらなかったです。そのため、その仕組を作って、使い方と実装に関して記事にまとめました。

## なぜJSONを型でバリデーションしたいのか？

`JSON.parse()`が`any`を返してしまうからです。
`any`のため、戻り値をどんな型にも代入可能です。プロパティが存在してなくても代入可能です。これでは、存在しないプロパティのアクセスしたときのエラーは実行時に出てしまいます。また、`parse()`の戻り値がプロパティの型が違っていても代入できてしまいます。せっかくのTypeScriptの型システムを活かせず、安全じゃないです。


## 理想の型のバリデーション
一番の理想は、型定義が与えられ、それだけでJSONをバリデーションすることです。
そのためには、TypeScriptの型を実行時に知れる必要があります。
なぜなら、バリデーションは実行時に行われるからです。実行時にJSONがその型に準拠しているかを検証することで型によるバリデーションが達成できます。
ただ、どうやらTypeScriptのポリシー的にそれが可能ではなそうなのです。

バリデーションライブラリの一つである[json-type-validation](https://github.com/mojotech/json-type-validation)でTypeScriptではランタイムでの型情報の提供がNon-goalsになっていると教えてくれます。おそらく以下の文章が指しているnon-goalだと思います。

> Add or rely on run-time type information in programs, or emit different code based on the results of the type system. Instead, encourage programming patterns that do not require run-time metadata.

（引用元： https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals#non-goals）

型システムの結果によって異なったコードが出来上がるようなことは、TypeScriptはしたくないようです。そのため、実行時にTypeScriptの型を知ることができず、実行時に型によるJSONのバリデーションができないです。

## 型バリデーションをする解決策

解決策は、[json-type-validation](https://github.com/mojotech/json-type-validation)がやっている方法になると思います。このライブラリでは型情報用のオプジェクトを作って、それを使ってバリデーションします。

ただ、ドキュメントを見る限り、**型情報用のオブジェクト(Decoder)と型定義の２つを書く必要がありそう**です。

今回は、型の構造を書けば静的に型を生成する仕組み+バリデーションを作ったので紹介したいと思います。

## GitHubリポジトリ
<https://github.com/nwtgck/ts-json-validator>

## 使い方

以下でGitHub経由でインストールできます。

```bash
npm install -S git+https://github.com/nwtgck/ts-json-validator#v0.1.2
```
（もう少し個人的に使ってみてから、npmに上げたいなと思ってます）

TypeScript 3.4.5を使ってます。

```ts
// 型バリデーション用のJSONのフォーマットの定義
const personFormat = obj({
  name: str,
  age: num
});

// 型のフォーマットからPerson型を導出
// （ここが一番のポイント！）
type Person = TsType<typeof personFormat>;

// JSON文字列からPersonオブジェクトを作成
// JSON.parse()より安全にパース
const p1: Person | undefined = validatingParse(personFormat, '{"name": "jack", "age": 8}');
// => {name: "jack", age: 8}
```

一番の重要なのが、`type Person = TsType<typeof personFormat>`です。変数`personFormat`を作れば、それに対応する型を導出できるところです。
この場合、以下のようにPerson型が導出されます。

```ts
// （自動でコンパイラが導出してくれる）
// (自分で定義する必要はありません)
type Person = {
  name: string,
  age: number
}
```

調べた範囲のTypeScript用のバリデーションライブラリは、ランタイムのためのJSONの構造情報と型定義の２つを作成する必要がありました。
保守のしやすさを考えると、どちらか一つで済むと嬉しいです。

`interface`が欲しい場合は、以下のようにするとできると思います。

```ts
interface IPerson extends TsType<typeof personFormat> {}
```


## 型安全なの？
導出した`Person`型がちゃんと思ったとおりに導出できているか、確認します。

### `age`の型を間違ってしまった場合
ちゃんと「`false`が`number`に代入できないよ」とコンパイル時に検出できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84451/56de531e-98f5-c88f-9cd0-ab0cbe441aad.png)

### `age`を書き忘れちゃったとき

ちゃんと「プロパティageが見つからないよ」とコンパイル時に検出できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84451/39f543cf-0e8b-2ac5-40b9-96671963d88c.png)


### 存在しないプロパティを書いてしまったとき

ちゃんと「'somethingElse'は存在しないよ」とコンパイル時に検出できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84451/a711034a-1b79-a108-a93d-47e9d72da5ef.png)

## 配列/入れ子/Literal Type/Union Type/タプルなどなど

色んな型を使う例です。
言葉の説明はコード内のコメントに書いています。

```ts
// 型のフォーマットを定義
const myObj1Format = obj({
  // 文字列配列 - string[]
  myStrs: arr(str),
  // 入れ子のオブジェクト - (myFlag: boolean)
  myObj: obj({
    // 真偽値
    myFlag: bool
  }),
  // Union Type - string | number | bool
  strOrNumOrBool: union(str, num, bool),
  // オブジェクト配列 - {prop1: number, prop2: string}[]
  myObjs: arr(obj({
    prop1: num,
    prop2: str
  })),
  // なくても良いプロパティ（myOptional?: number）
  myOptional: opt(num),
  // Literal Type - (myLiteral: 'MY_LITERAL')
  myLiteral: literal('MY_LITERAL' as const),
  // Literal TypeのUnion - ('red' | 'blue')
  myLitUnion: union(literal('red' as const), literal('blue' as const)),
  // null許容 - (string | null)
  myNullable: union(str, nul),
  // タプル - [string, boolean, number]
  myTuple: tuple(str, bool, num),
});

// 型を導出する
type MyObj1 = TsType<typeof myObj1Format>;
```

`interface`がほしいときは、以下のようにすると良いと思います。

```ts
interface IMyObj1 extends TsType<typeof myObj1Format> {}
```


以下のように、`MyObj1`型の変数を作れます。

```ts
const myObj1: MyObj1 = {
  // 文字列配列 - string[]
  myStrs: ['apple', 'orange'],
  // 入れ子のオブジェクト - (myFlag: boolean)
  myObj: {
    // 真偽値
    myFlag: true
  },
  // Union Type - string | number | bool
  strOrNumOrBool: 'this is a text',
  // オブジェクト配列 - {prop1: number, prop2: string}[]
  myObjs: [
    {prop1: 9, prop2: 'abc'},
    {prop1: 43, prop2: 'hello'},
  ],
  // なくても良いプロパティ（myOptional?: number）
  // myOptional: 43, // なくても良いからコメントアウトできる
  // Literal Type - (myLiteral: 'MY_LITERAL')
  myLiteral: 'MY_LITERAL',
  // Literal TypeのUnion - ('red' | 'blue')
  myLitUnion: 'blue',
  // null許容 - (string | null)
  myNullable: null,
  // タプル - [string, boolean, number]
  myTuple: ['my string', false, 32],
};
```

### コンパイラで間違いが検出できる確かめる

以下のように、`prop1`にわざと文字列を入れると、ちゃんと「stringがnumberに代入できない」とわかります。IDEでの赤波線が`prop1`に出るように`TsType`を作る話など、詳しい実装は後述します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84451/8658f0ef-ed9c-5507-47e6-2c8348f826d1.png)

以下のように、Literal Typeの間違いも検出します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84451/eec45b77-128f-4d59-190e-c7dfb9d51524.png)


以下のように、`[string, boolean, number]`のタプルの真ん中の型を間違えると、これもコンパイル時に検出できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84451/9c72ad38-33fc-cc0f-10b4-8ee1a365a62b.png)

プロパティ`myStrs`を消してみると、以下のように、「'myStrs' is declared here」と言われ、たりないことがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/84451/f6a2332f-4557-68ab-ccfe-f457ebdfb2c8.png)

他にも色々と試してみて、`type MyObj1 = ...`で型が導出できているか確認できます。

## `TsType<>`は`obj`以外の配列などでも使える

今までの例では、`obj({...})`で作った変数で`TsType<>`をしてきました。
場合によってはkey-valueのオブジェクト以外に、配列をつかいたときなどもあると思います。（APIによってはJSONの配列が返ってくることもよくあります。）

`TsType<>`の使い方は変わらず、以下のように使えます。
以下は簡単のため`string[]`ですが、オブジェクトの配列にしたり、好きにできます。`TsType<>`の実装などは後述です。

```ts
// string[]
const strArrayFormat = arr(str);

// string[]として導出される
type MyStringArray = TsType<typeof strArrayFormat>;

// 普通の文字列配列
const myArray: MyStringArray = ['hello, world', 'apple orange'];
```

**その他、`num`でも`str`でも`union`でも`tuple`でも同様に`TsType<>`で純粋なTypeScriptの型をを導出**できます。

## 実装面 - 少し型レベルの話

**`TsType<typeof personFormat>`でどうやって自動で型が導かれるのかの話**です。


これは実行時の`typeof`とは違うと思います。コンパイル時に解決されるものだと思います。
以下のようにすると`N`型は`number`型になります。

```ts
const n: number = 10;
type N = typeof n;
```


`TsType<...>`は現在のバージョン0.1.2だと、`type TsType<J extends Json> = J['tsType'];`となってます。
そのため、`type Person = (typeof personFormat)["tsType"]`でも同じように`Person`型が取れるはずです。


この`["tsType"]`の記法のシンプルな例は以下のようなものです。
以下の`MyType["stringType"]`は`string`型になります。

```ts
type MyType = {
  stringType: string;
}
type MyString = MyType["stringType"];
```

`type Json`というものに対して`["tsType"]`しています。現在の`Json`の定義は以下のUnion Typeになっています。つまり、`Null["tsType"]`や`String["tsType"]`などで型が返ってくるイメージです。

```ts
export type Json =
  Null |
  Boolean |
  Number |
  String |
  Literal<unknown> |
  Union |
  Tuple |
  Optional<any> |
  Array<any> |
  Object<{[key: string]: Json}>;
```

`Null["tsType"]`や`Object<...>["tsType"]`によって返ってくる型がが変わります。そのため**型レベルの関数**のようなイメージになります。Haskellだと型族が似たような概念なのではないかと思います。

### `TsType<typeof strArrayFormat>` === `string[]`を追いかける

ちょっと前に登場した`const strArrayFormat = arr(str);`の`TsType<typeof strArrayFormat>`が`string[]`になることを例にとって動作を追ってみます。

まずは、`TsType<>`を`["tsType"]`に置き換えて、以下がなりたちます。
`TsType<typeof strArrayFormat>` === `(typeof strArrayFormat)["tsType"]`

次に`typeof strArrayFormat`を求めます。`typeof strArrayFormat`は単純に`strArrayFormat`の型です。関数`arr()`と変数`str`の型定義を見ることでわかります。


```ts
const str: String
function arr<T extends Json>(elem: T): Array<T>
```

ここから`arr(str)`は`Array<String>`型になることがわかります。
つまり、`(typeof strArrayFormat)["tsType"]`===`(Array<String>)["tsType"]`が成り立ちます。


`(Array<String>)["tsType"]`を求めます。以下の`Array<T>`の定義の`tsType`を見ると、`T['tsType'][]`です。ここでは`T`===`String`です。そこで`T`に`String`を当てはめると、
`(Array<String>["tsType"])` === `(String['tsType'])[]`になります。

```ts
export interface Array<T extends Json> {
  tsType: T['tsType'][];
  runtimeType: {
    base: 'array',
    element: JsonRuntimeType
  };
}
```

最後に、`(String['tsType'])[]`の`String['tsType']`を求めます。以下が`String`の定義です。`tsType`は`string`です。そのため、`String['tsType']` === `string`です。

```ts
export interface String {
  tsType: string;
  runtimeType: 'string';
}
```

最終的に`(String['tsType'])[]` === `string[]`となります。

イコールを全部つなげると、`TsType<typeof strArrayFormat>` === ... === `string[]`となることが分かります。


### `tsType`の定義

`tsType`の実装はとても自然な定義になっています。

* 数値なら`tsType: number`です
* 配列の場合は今見たように`tsType: T['tsType'][]`です。
* ３つのUnionなら、`tsType: T1['tsType'] | T2['tsType'] | T3['tsType']`です。
* ...


### key-valueのオブジェクトの`tsType`

データ型で、一番複雑なのは、key-valueになっているオブジェクトの形式だと思います。
少し複雑にさせるのは、オプショナルなプロパティの存在の影響です。（例：`{myProp?: string}`）
以下がその定義です。

```ts
type NonOptionalKeys<Obj extends {[key: string]: Json}> = {
  [K in keyof Obj]: undefined extends Obj[K]['tsType'] ? never : K
}[keyof Obj];

type OptionalKeys<Obj extends {[key: string]: Json}> = {
  [K in keyof Obj]: undefined extends Obj[K]['tsType'] ? K: never;
}[keyof Obj];

type NonOptionalObj<Obj extends {[key: string]: Json}> = Pick<Obj, NonOptionalKeys<Obj>>;
type OptionalObj<Obj extends {[key: string]: Json}> = Pick<Obj, OptionalKeys<Obj>>;

export interface Object<O extends {[key: string]: Json}> {
  tsType: {
    [K in keyof NonOptionalObj<O>]: O[K]['tsType'];
  } & {
    [K in keyof OptionalObj<O>]?: O[K]['tsType'];
  };
  runtimeType: {
    base: 'object',
    keyValues: {[key: string]: JsonRuntimeType}
  }
}
```

`OptionalKeys<>`は`T | undefined`ように、`undefined`を含まないプロパティのキーの集まりになります。`NonOptionalKeys<>`はその補集合です。`Pick<>`はTypeScript標準である型です。`Pick<Obj, Keys>`で`Obj`中で`Keys`なkey-valueを集めた型になります。これでオプショナルなプロパティとそうでないものを分けています。