TypeScript: ユーザ定義タイプガードでanyにinterfaceとしての型情報を与える

本稿ではTypeScriptのユーザ定義タイプガードを使って、anyな値を任意のinterfaceとして扱えるようにする方法を説明する。

例えば、次のようなインターフェイスをもったオブジェクトを扱いたいとする。

```ts
interface Foo {
  doSomething(arg1: string): void
}
```

何らかの事情により、このオブジェクトは`any`として渡ってる。

```ts
const foo: any = { doSomething(arg1) {} }
```

このままでも、`foo.doSomething(value)`と書いて呼び出すことができるし、コンパイルも通る。

しかし、`foo`は`any`型なので、IDEでの補完も効かないし、`value`にちゃんと`string`型を渡せているか不安になる。また、実行時本当に`Foo`型のように振る舞えるオブジェクトが渡ってこない可能性の考慮が抜けている。

ユーザ定義タイプガードを使うと、これらの課題を解決できる。`any`型だった`foo`に、`Foo`型の値であるという情報をコンパイラに伝える。それにより、IDEでメソッド名が補完されるようになるし、実行時の安全性も高くなる。

## `Foo`インターフェイスのための、ユーザ定義タイプガードの実装例

```ts
// Fooインターフェイス再掲
interface Foo {
  doSomething(arg1: string): void
}

// タイプガード関数
const isFoo = (value: unknown): value is Foo =>
  value !== null &&
  typeof value === 'object' &&
  typeof (value as { doSomething?: unknown }).doSomething === 'function' &&
  (value as { doSomething: Function }).doSomething.length === 1
```

## 説明

### 理解するためにJSコードを見てみよう

上記のコードをJSにコンパイルすると次のようになる:

```js
const isFoo = (value) => 
    value !== null &&
    typeof value === 'object' &&
    typeof value.doSomething === 'function' &&
    value.doSomething.length === 1;
```

このJSコードは見てのとおり、`value`が`doSomething`メソッドを持つか順を追って確認しているだけだ。実行時は、ただそういったチェックを入念にしているに過ぎず、別段特別なコードではない。型がゆるふわなJSでは昔からよくやられていたチェックだ。

#### JSコードの各行の説明

念の為、上記行の意味を説明しておく。

1行目: **`value !== null`**

`typeof null`は`"object"`になるため、`null`だけ特別に弾く条件だ。

2行目: **`typeof value === 'object'`**

ざっくり`value`がオブジェクトであることを確認している。これにより次段以降で、`.`でプロパティにアクセスできることが保証される。

3行目: **`typeof value.doSomething === 'function'`**

`value`オブジェクトに`doSomething`プロパティが生えていること、つまり`undefined`でないことを確認するとともに、それが関数であることを確認している。これにより、次段以降で、`value.doSomething`に直接アクセスしてもエラーにならないことが保証される。

4行目: **`value.doSomething.length === 1`**

3行目までで`value.doSomething`が`Function`であることは確認済み。実行時にできるのこりのチェックとしては、引数の数をチェックするくらいだ。

### TypeScriptコードのタイプガードを改めて見てみる

TypeScriptのコードに戻ろう。

```ts
const isFoo = (value: unknown): value is Foo =>
  value !== null &&
  typeof value === 'object' &&
  typeof (value as { doSomething?: unknown }).doSomething === 'function' &&
  (value as { doSomething: Function }).doSomething.length === 1
```

JSコードとのわずかな違いは、`as { doSomething?: unknown }`と`as { doSomething: Function }`の部分だけだ。これらはコンパイルを通すために必要なものになる。

まず、3行目の`as { doSomething?: any }`だが、コンパイラは、

```ts
  value !== null &&
  typeof value === 'object'
```

までのコードを読んだ結果、`value`が`object`であることを知る。

次の`typeof value.doSomething`に繋げたいわけだが、コンパイラは`value`に`doSomething`が生えていることを確証を得ていない。そのため「Property 'doSomething' does not exist on type 'object'.」というコンパイルエラーを吐く。

それを避けるために、`value as { doSomething?: unknown }`と書いて、コンパイラに`doSomething`が生えている可能性を示唆してやる。`value as Foo`でも問題ないが、ここは謙虚に2行目までに判明している事実を`as`以降に書いている。

`{ doSomething?: unknown }`という型は`doSomething`が生えているかもしれないし、生えていないかもしれない。そして、その型は不明という意味合いだ。

4行目の`value as { doSomething: Function }`も同様に、`.length`にアクセスするためにコンパイラに`doSomething`が関数であることを教えている。


## `isFoo`のテスト

タイプガードは実行してみないと正しく動作しているのかわからないので、`isFoo`の動作確認を行っておく。本来は単体テストでチェックしておくようなものだが、ここでは簡易的に`console.log`で目視確認するに留める。

```ts
// Foo型でないもの
console.log(isFoo(undefined) === false)
console.log(isFoo(null) === false)
console.log(isFoo(1) === false)
console.log(isFoo(NaN) === false)
console.log(isFoo(true) === false)
console.log(isFoo('str') === false)
console.log(isFoo({}) === false)
console.log(isFoo(['array']) === false)
console.log(isFoo({ doSomething: undefined }) === false)
console.log(isFoo({ doSomething: null }) === false)
console.log(isFoo({ doSomething: 1 }) === false)
console.log(isFoo({ doSomething: NaN }) === false)
console.log(isFoo({ doSomething: true }) === false)
console.log(isFoo({ doSomething: 'str' }) === false)
console.log(isFoo({ doSomething: {} }) === false)
console.log(isFoo({ doSomething: ['array'] }) === false)
console.log(isFoo({ doSomething: () => {} }) === false)
console.log(isFoo({ doSomething: (arg1: any, arg2: any) => {} }) === false)

// Foo型なもの
console.log(isFoo({ doSomething: (arg1: any) => {} }) === true)
```