LoginSignup
180
121

More than 1 year has passed since last update.

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

Last updated at Posted at 2017-04-20

注: この記事は結構古いです Unknown にアクセスしたい。- TypeScript で外部データを型安全に読み込む。 - Qiita も見て下さい。

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

失敗例

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

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 を返す関数の事で、次のように書く。

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 型であるとみなす。

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 'エラー';
}

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

// 上手く行く例 -> 'たくみお姉さん'
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 を書いてもコンパイルは通る。

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

組み込み型の Type Guard

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

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

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

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

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

// 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;

ガードを入れた場合。

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"));

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

参考

180
121
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
180
121