【TS】今さら聞けないユーザ定義型ガード
はじめに
今回はTypescript
におけるユーザ定義型ガードについてご紹介します。
独自に定義した型であるかどうかの判定を開発者自身が定義できる便利機能になっています。
やりたいこと
RPGの道具・武器の型定義を題材として扱います。
簡単に、以下のような型を定義したとします。
type Item = {
name: string;
use: () => void;
}
type Weapon = {
atack: () => void;
} & Item;
Item
型とWeapon
型があり、Item
が道具でWeapon
が武器として振る舞います。
どちらにも共通してname
とuse
というプロパティがあり、Weapon
にだけatack
というプロパティがあります。
実際にインスタンスとして「やくそう」と「こんぼう」、「バールのようなもの」を定義した場合は以下のようになります。
バールのようなものは、普段は道具ですが稀に武器(凶器)として扱われるためItem
とWeapon
のユニオン型になっています。
const item1: Item = {
name: 'やくそう',
use: () => console.log("やくそうを使った!")
}
const item2: Weapon = {
name: 'こんぼう',
use: () => console.log('こんぼうを使った'),
atack: () => console.log('こんぼうで殴った!10ダメージ!')
}
const item3: Item | Weapon = {
name: 'バールのようなもの',
use: () => console.log("バールのようなもののようだ"),
//atack: () => console.log("バールのようなもので殴った!9999ダメージ!")
}
ここまでを前提として、下記のようにItem | Weapon
型の引数を受けて「道具ならuse
」を、「武器ならatack
」を実行するuse
関数を作りました。
const use = (item: Item | Weapon) => {
// Itemならitem.use()を実行
// Weaponならitem.atack()を実行
}
ここで問題となってくるのが「item
をどうやってItem
型かWeapon
型か判定するか」です。
いくつか思いつきそうな方法を試してみます。
①「プロパティの有無を確かめる?」
直感的に思いつくのがatack
プロパティの有無です。
!!item?.atack
の条件でif
文を書いてみましたが、構文エラーになります。
const use = (item: Item | Weapon) => {
if(!!item?.atack) {
// ↑の文がエラーになる
// Property 'atack' does not exist on type 'Item'.(2339)
}
}
どうやら解析上は、item
がItem
型である可能性が排除できないのでatack
にアクセスできないようです。
②「typeofで判定すればいいのでは?」
ならばと条件分をtypeof item === 'Weapon'
に変更してみました。
先ほどと同様にこちらも構文エラーが発生します。
const use = (item: Item | Weapon) => {
if(typeof item === 'Weapon') {
// ↑の文がエラーになる
// This condition will always return 'false' since the types '"string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"' and '"Weapon"' have no overlap.(2367)
}
}
typeof
では解析するインスタンスの型を厳密に判定するこはできない(ここでは"object"
として判定されるだけです)ためこのようなエラーがでます。
そもそもItem
だろうがWeapon
だろうがobject
型には違いないので、typeof
の判定では意味を成しません。
③「any型にした上で①のやり方をすればいけるのでは?」
①の方法の変形で、引数の型をany
にしてみました。
const use2 = (item: any) => {
// 構文エラーにならないが・・・
if(!!item?.atack) {
// これは本当にWeapon型のatack()なのか?
item.atack();
}
else {
// use()がないケースが出てくるのでは?
item.use();
}
}
構文エラーは出なくなりましたが、だいぶガバガバな関数になってしまいました。
any
を引数に取るため、Item
やWeapon
型以外のインスタンスを渡して実行することができてしまい、atack
のプロパティがあってもWeapon
型である保証がありません。
たとえばモンスターを表すMonster
型を作ったとしても同名のプロパティを持っていそうです。
また、atack
がなかったとしてもItem
型であるとは限らないのでuse
プロパティを持っていない可能性もあります。
ユーザ定義型ガードを使おう
こういった場合に便利なのがユーザ定義型ガードです。
ユーザ定義型ガードを簡単に説明すると 「開発者自身が何をもってその型であるかを定義できる」 ということです。
Weapon
型の例でいうと「atack
プロパティがあればWeapon
型とする」といった具合です。
ユーザ定義型ガードの書き方は簡単で、下記のように引数 is 型名
を戻り値にします。
const isWeapon = (item: any): item is Weapon => {
// Weapon型に強制キャストしてatackプロパティがあればWeapon型とする
return !!(item as Weapon)?.atack
}
このisWeapon
を使って条件分を組み立てることでuse
の引数をany
にすることなく実装できます。
const use = (item: Item | Weapon) => {
// エラーなし!
if(isWeapon(item)) {
// 以降itemはWeapon型として認識される
item.atack();
}
else {
item.use();
}
}
まとめ
今回はユーザ定義型ガードについてご紹介しました。
型判定を自由に記載できるため今回の例のように使い所は多いのかなと思います。
注意点としては自分で書いた型判定が誤っていると、意図しない挙動になってしまう可能性があります。
極端な例ですがisWeapon
は常にtrue
を返していても構文エラーにはならないので、その場合はItem
型でもWeapon
として判定されてしまうようになります。
この辺りは自由に定義できるメリットの裏返しになっているので、十分に注意して扱いましょう。
Discussion
とても分かりやすい記事ありがとうございました。
attack
プロパティがWeapon
にしか存在しないのであれば、ユーザー定義型ガードの戻り値!!(item as Weapon)?.attack
はin
を利用して'attack' in item
とも書けそうだなと思いました。参考:
コメント、コード提供ありがとうございます!
in
をあまり使ったことがなかったので勉強になります!isNullOrUndefined
のメソッドをユースケースとして定義し、チャレンジしてみました。demo
簡単ですが、以上です。
Jestでテストを書く際に独自型の判定ケースを作成したかったので、こちらの記事の内容がわかりやすくて役に立ちました。ありがとうございます!