TypeScriptの幽霊型

# TypeScriptの幽霊型

[幽霊型](https://kowainik.github.io/posts/haskell-mini-patterns#phantom-type-parameters)：追加の型変数を導入することで、型に含まれる情報を増やす技法。

今回は簡単なデコーダの例を通じ、幽霊型の用途を紹介する。

## デコーダとは？

デコーダとは入力の型を解析する関数だ。

デコーダの型はこちら：

``` ts
type Decoder<Input, Output> = (input: unknown) => Result<Output, Error>
```

まず出力の型を見ると、`Result`であることがわかる。`Result`とは、入力の処理に問題が発生すれば、エラーを返し、なければ結果を返す：

``` ts
type Result<T> = Ok<T> | Error

type Ok<T> = { tag: 'ok', value: T }

type Error = { tag: 'error', text: string }
```

入力の`input`は`unknown`であり、`Output`はそうでないことから、デコーダとは入力の型を分析する関数だとわかる。

どうやって分析を行うかというと、narrowing を行う。

### [narrowing](https://www.typescriptlang.org/docs/handbook/2/narrowing.html)

JavaScriptとTypeScriptには動的に型を判明する[`typeof`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/typeof)関数がある。

``` ts
if (typeof hoge === 'string') {
  // ここでは hoge＝文字列
}
```

これで動的な方法を使い、コンパイル時に型を推論できる。これを narrowing という。

では、narrowing を尽くし、複雑な型まで推論する方法を解説する。

## 簡単な幽霊型を作る

もう一度デコーダの型を見ましょう。

``` ts
type Decoder<Input, Output> = (input: unknown) => Result<Output, Error>
```

早速だが、上記における幽霊型はどれか理解できましたか。しばらく考えてみてください。

ヒント：幽霊型は右辺に含まれていない。

前述した通り、`input`は`unknown`だから、型がわからないはずだ。だが narrowing による型推論を使えば、`input`の形が段々明確になる。
例えば：

``` ts
const stringDecoder = (input: unknown) => {
  if (typeof input === 'string') {
    // `ok`は`Ok`型のコンストラクタ
    return ok(input)
  }
  else {
    // `error`は`Error`型のコンストラクタ
    return error('Not a string.')
  }
}
```

入力の`typeof`が`string`でなければエラーを返すので、返り値はエラーでなければ入力はやはり文字列だったということがわかる。

つまり、上記の型は`Decoder<string, string>`である。以下はその使い方：

``` ts
stringDecoder('string') -> Ok<string>

stringDecoder(0) -> Error
```

## オブジェクトの幽霊型

`typeof`を使えば文字列、数字やブール型のためのデコーダが簡単に作成できるようだ。あとは少し工夫してオブジェクト型のためのデコーダを作りましょう。

次に複数の問題を出すが、必ずやってみてください。ただ答えを見るだけでは理解が得られない。

オブジェクトのデコーダの型はこちら：

``` ts
const objectDecoder = (decoders: D) => (input: unknown) => { ... }
```

`objectDecoder`の型の例：

``` ts
const decoders = {
	a: stringDecoder,
	b: numberDecoder,
	c: objectDecoder({ d: boolDecoder })
}

objectDecoder(decoders)

=> Decoder<{ // 入力の型
	a: string,
	b: number
	c: {
		d: bool
	}
}, {　       // 出力の型
	a: string,
	b: number
	c: {
		d: bool
	}
}>
```

### 第１問

`decoders`とは入力オブジェクトのキーに対するデコーダなら、型`D`はなんだ？ヒント：オブジェクト型を使うといい。

<details>
<summary>答え</summary>

``` ts
// ObjectKey = string | number | symbol
type D = { [Key in ObjectKey]: Decoder<unknown, unknown> };
```

`D`とは任意のキーに対してデコーダを提供するオブジェクトの型である。

なぜ入力におけるフィールドではなく、任意のフィールドにするのかというと、`input`は`unknown`型だからそもそも入力フィールドがわからない。

言い換えると、`decoders`のフィールドをもとに`input`のフィールドを判明する。

</details>

次に、`objectDecoder`の返り値の型を考える。

そのために、二つの型を定義しないといけない：

1. InputType: デコーダ型から入力型を抽出する型
2. OutputType: 同様、デコーダ型から出力型を抽出する型

### 第２問

`InputType`と`OutputType`を定義してみてください。

<details>
<summary>答え</summary>

``` ts
type InputType<D> = D extends Decoder<infer Input, unknown> ? Input : never;
type OutputType<D> = D extends Decoder<unknown, infer Output> ? Output : never;
```

まず、ここの`extends`は`===`の意味に近い。とりあえず`D`はデコーダかどうか知りたいわけだ。

次に`infer`だが、文字通りコンパイラに対して「この型を推論してみて」という命令になっている。以下の条件二つが揃っていれば、`infer`を使用してください：

1. 型の定義内に型パラメータを参照したい
2. 型が事前にわからないため型パラメータとして渡せない
3. 渡した型パラメータの一部だけ参照したい（例：`number[]`の`number`なら`(infer U)[]` -> `U === number`）

以上の例では、`Input`がわからないからこそ`InputType`に推論してもらう。つまり、`infer Input`。

最後に、`unknown`の意味だが、単純に「こいつは無視したまえ」ということだ。

`InputType`型を文のように読み上げれば：`InputType`のパラメーター`D`はデコーダだったら、その入力の型を返す。
</details>

### 第３問

`const objectDecoder = (decoders: D): R => ...`の返り値`R`の型はな〜んだ？

ヒント：`InputType`と`OutputType`はここで役に立つ。

ヒントその２：オブジェクト型のキーを参照したければ`keyof`を使うといい。

<details>
<summary>答え</summary>

まず、`R`はデコーダである。したがって、本当に気になるのは`type R = Decoder<Input, Ouput>`の`Input`と`Output`だ。

もちろん、`objectDecoder`は文字通りオブジェクトをデコードする関数だから、入力も出力もきっとオブジェクトだろう。だが、フィールドの型を特定しないといけない。

そこで、`keyof`を使って`D`からキーを取り出し、先ほど定義した`InputType`と`OutputType`を適用する：

``` ts
const objectDecoder = (decoders: D): Decoder<
		{ [Key in keyof D]: InputType<D[Key]> },
		{ [Key in keyof D]: OutputType<D[Key]> }
	>
	 => ...
```

`D[Key]`という構文はあくまで`obj[field] = ...`の型版であり、「このキーに対するフィールドの型ちょうだい〜」と読むことができる。

</details>

## まとめ

これで`objectDecoder`の型の定義が完成した。関数本体はまだ書いていないが、かなり難しく幽霊型とほとんど関係がないので別の機会にしましょう。

この記事で紹介したデコーダを使うことで、`any`か`unknown`から意味を持つ型をコンパイル時に取り出し、いつも通りTypeScriptの型チェックに機能してもらうことができる。APIからのデータを扱う際に特に役立つ。