TypeScript の Type Guard を使ってキャストいらず

注: この記事は結構古いです [Unknown にアクセスしたい。- TypeScript で外部データを型安全に読み込む。 - Qiita](https://qiita.com/propella/items/9a00d1ab1c4a10e6b7e4) も見て下さい。

TypeScript にもだいぶ慣れてきた。慣れてくると間違えやすい部分もはっきりしてきた。それが Type assertions (キャスト)。Type assertions を使うと、実際のデータがどうであろうが強制的に型情報を書き換えてしまえるので、有り難い Typescript の型チェックをすり抜けてしまう。Typescript では Type assertion を使う代わりに実行時型チェックを強制する Type Guard という仕組みがあるので試してみた。

## 失敗例

まずは Type assertions を使ったよくある失敗例。JSON データの内容によって別の型として扱おうとしている。

```javascript
interface Song {
  type: 'song';
  name: string;
  singer: string;
}

interface Dance {
  type: 'dance';
  name: string;
  dancer: string;
}

// 元のコード
const getPerson0 = (item: any): string => {   // 引数が any なので何でもあり
  if (item.singer)                    // 一見型チェックをちゃんとしているが。。。
    return (item as Song).singer;
  else                                // Song でも Dance でもないケースを見逃していた！
    return (item as Dance).dancer;
}

// 上手く行く例 -> 'たくみお姉さん'
console.log(getPerson0({type: 'song', name: 'つるのワルツ', singer: 'たくみお姉さん'}));
// 上手く行く例 -> 'りさお姉さん'
console.log(getPerson0({type: 'dance', name: 'パント', dancer: 'りさお姉さん'}));
// エラー処理抜け -> undefined
console.log(getPerson0({type: 'comedy', name: '数え天狗', comedian: 'だいすけお兄さん'}));
```

この例ではどんな JSON データでも受け付けたいので、getPerson0 の引数の型を any にしているが、Song にも Dance にもならないケースを見逃していた。そこで以下の方法でTypeScript に抜けをチェックしてもらう。

* 引数として any の代わりに {} を使う。number 等の値や不用意なメンバーアクセスの可能性を排除出来る。
* Type Guard を使う

## Type Guard を使った例

Type Guard とは、あるデータの型が確定された時に true を返す関数の事で、次のように書く。

```javascript
const isSong = (item: any): item is Song =>
  item.type === 'song' && item.singer !== undefined;

const isDance = (item: any): item is Dance =>
  item.type === 'dance' && item.dancer !== undefined;
```

(なお、上のような関数の返り値で `x is Type` と書く文法自体は Type Predicate と呼ぶ。)

この関数 isSong の戻り値の型 item is Song は、「引数 item が Song 型の時 true を返す」事を示している。isSong(item) でチェックされた引数を TypeScript は Song 型であるとみなす。

```javascript
const getPerson = (item: {}): string => { // any の代わりに {} を使う。
  if (isSong(item))         // isSong によってこの分岐では item の型は Song になる。
    return item.singer;     // (item as Singer) が無くてもエラーにならない。
  else if (isDance(item))   // isDance によってこの分岐では item の型は Dance になる。最初の例で忘れていた処理。
    return item.dancer;     // この分岐が無いと TypeScript は item を {} 型だとみなすので item.dancer はコンパイルエラーになる。
  else
    return 'エラー';
}
```

これでちゃんとエラー処理を忘れず書くことが出来る。

```javascript
// 上手く行く例 -> 'たくみお姉さん'
console.log(getPerson({type: 'song', name: 'つるのワルツ', singer: 'たくみお姉さん'}));
// 上手く行く例 -> 'りさお姉さん'
console.log(getPerson({type: 'dance', name: 'パント', dancer: 'りさお姉さん'}));
// ちゃんとエラーになった。-> エラー
console.log(getPerson({type: 'comedy', name: '数え天狗', comedian: 'だいすけお兄さん'}));
```

注意点としては、Type Guard を使っても結局型チェックは手で書くので、そこで間違えると TypeScript の推論は役に立たない。わざと以下のように間違った Type Guard を書いてもコンパイルは通る。

```javascript
const isSong = (item: any): item is Song => true;
```

## 組み込み型の Type Guard

実は number や string など、組み込み型では最初から Type Guard が用意されている。単に typeof で型をチェックすれば良い。

引数が any のお行儀の悪いコード

```javascript
const addThree0 = (value: any) => value + 3;

// 正常系 -> 45
console.log(addThree0(42));
// 文字列でが暗黙に変換されてしまい意図せぬ結果に。-> 423
console.log(addThree0("42"));
```

ガードなしで引数を number | string にした場合、コンパイルエラーになる。

```javascript
// The left-hand side of an arithmetic operation must be of type 'any', 'number' or an enum type.'
const addThree1 = (value: number | string) => value + 3;
```

ガードを入れた場合。

```javascript
const addThree = (value: number | string) =>
  typeof value === 'number' ? value + 3 // typeof で number かどうかをチェック。
  : parseInt(value) + 3;                // それ以外は自動的に string と判断される。

// 正常系 -> 45
console.log(addThree(42));
// ちゃんと動く -> 45
console.log(addThree("42"));
```

これでお行儀のよいコードの書き方が分かってきた。

## 参考

* https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types