注: この記事は結構古いです 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"));
これでお行儀のよいコードの書き方が分かってきた。