とある休日
娘(5歳)「ねえパパ」
ワイ「なんや?娘ちゃん」
娘「あのね」
娘「TypeScriptを勉強してたら、型ガードっていう概念が出てきたんだけど」
娘「型ガードって何?」
ワイ「ああ、それはな」
ワイ「片面がクッキーで、もう片面がチョコのやつや」
ワイ「ほんで、チョコの面は船の絵みたいになってんねや」
娘「食べてみた〜い」
よめ太郎「それアルフォートやないかい」
ワイ「Oh...よめ太郎...」
よめ太郎「そうやなくて」
よめ太郎「型ガードの話をしてんねん」
よめ太郎「変われ、わしが説明する」
選手交代
よめ太郎「そんで娘ちゃん」
よめ太郎「具体的にどういうタイミングで型ガードが気になったん?」
娘「ええとね」
娘「実は今、遊園地のオンライン申し込みシステムを作っててね」
娘「その遊園地では、12歳以下の子供は料金が半額になるから」
娘「あるユーザーが12歳以下かどうかを判定する関数を書こうとしてたの」
関数名: isChild
- ユーザーが子供料金の対象かどうかを判定する関数
- ユーザーが12歳以下であれば
true
を返す- ユーザーが13歳以上であれば
false
を返す
娘「↑こんな関数が必要なの」
よめ太郎「なるほどな」
娘「それでね」
娘「年齢を登録していないユーザーもいるから」
娘「ユーザーを表す型は↓こんな感じで書いたの」
type User = {
name: string
age: number | null
}
よめ太郎「なるほどな」
よめ太郎「ユーザーの年齢はnull
の場合もあるってことやな」
娘「うん」
娘「それで、さっきの子供判定関数を書こうとしてみたの」
const isChild = (user: User): boolean => {
return user.age <= 12
// -> true または false
}
よめ太郎「なるほどな」
よめ太郎「引数としてユーザーを受け取って」
よめ太郎「12歳以下かどうかを真偽値で返すんやな」
娘「うん」
娘「でも、ここでTypeScriptのエラーが出ちゃったの1」
よめ太郎「なるほどな」
よめ太郎「null
かもしれん値を、数値と比較しようとしたからやな」
娘「調べてみたところ」
娘「こういうときには型ガードが必要みたいなんだけど・・・」
ワイ「ちゃうちゃう」
ワイ「こういうときに必要なのは、型アサーションや」
型アサーションしてみる
ワイ「user.age
を数値として扱うために」
ワイ「user.age as number
って書けばええんや」
娘「なるほどね!」
「
user.age
は、ここではnumber
型やで!」
娘「↑こんな風に主張するんだね」
娘「アサーションって、主張とか断言って意味だもんね!」
娘「やってみるね!」
娘「あっ!ちゃんとエラーが消えた!」
娘「パパ、ありがとう!」
ワイ「ゲヘヘ」
よめ太郎「いや全然あかんで」
ワイ「なんでや」
よめ太郎「試しに、今の状態のisChild
関数を実行してみい」
ワイ「ぐぬぬ、やってみたるわ」
const ojisan: User = {
name: 'やめ太郎',
age: null
}
ワイ「↑まずは、こうやな」
ワイ「試しに1人ユーザーを作ってやるんや」
ワイ「年齢は登録していないパターンにしといたで」
ワイ「ほんで、次は・・・」
console.log(isChild(ojisan))
ワイ「↑こうや!」
ワイ「isChild
関数で、ojisan
が子供かどうかチェックしたるんや!」
ワイ「結果は・・・!」
true
ワイ「ファッ!?」
ワイ「true
!?」
ワイ「ojisan
が子供扱いになってもうた・・・」
よめ太郎「そうやで」
よめ太郎「null
と12
を比較したら」
よめ太郎「null
の方が小さいことになってしまうねん」
ワイ「マジか」
ワイ「実際には年齢を登録してないだけなのに」
ワイ「12歳以下ってことになってまうやんけ・・・」
よめ太郎「せやからnull
と数値の比較とか、せんほうがええで」
よめ太郎「そのために、わざわざコンパイラくんが・・・」
オブジェクトは 'null' である可能性があります。
よめ太郎「↑こんなエラーを出してくれてたんや」
ワイ「なるほどな・・・」
よめ太郎「null
の可能性もあるのに」
よめ太郎「無理やり型アサーションでコンパイラに言うことを聞かせて」
よめ太郎「数値扱いなんてしたらあかんねん」
ワイ「ぐぬぬ・・・」
よめ太郎「そこで、型ガードや」
型ガードしてみる
よめ太郎「こうや!」
const isChild = (user: User): boolean => {
if (user.age === null) return false // <- 追加
return user.age <= 12 // <- 型アサーションはしない
}
ワイ「ほうほう・・・」
ワイ「年齢がnull
の場合には、すぐにreturn false
して」
ワイ「子供じゃない扱いにしたるんやね」
よめ太郎「せや」
よめ太郎「user.age
がnull
、つまり年齢が未登録の場合は」
よめ太郎「子供料金を適用したらあかんからな」
ワイ「せやな」
よめ太郎「こうすることで、その下の行のuser.age
は」
よめ太郎「null
である可能性が排除されるから・・・」
return user.age <= 12
よめ太郎「↑この部分の型エラーは出なくなるんや」
よめ太郎「数値が来て欲しいところにnull
が来てしまう可能性を排除できたんや」
ワイ「ほんまや」
オブジェクトは 'null' である可能性があります。
ワイ「↑このエラーが表示されんくなっとるな」
ワイ「型アサーションも消したのに」
ワイ「ちゃんとuser.age
が数値として認識されとるみたいやな」
娘「そっか」
娘「年齢がnull
だった場合には早期リターンされちゃうから」
娘「その先の行までたどり着けないはずだもんね」
娘「この行ではuser.age
は必ずnumber
型だ、ってことが保証2されたから」
娘「エラーが出なくなったんだね!」
const isChild = (user: User): boolean => {
// user.age が null なら、次の行で処理は終了する
if (user.age === null) return false
// 従って、ここでは user.age は null になる可能性はなく
// number となる
return user.age <= 12
}
娘「↑こういうことだね!」
よめ太郎「そういうことや」
よめ太郎「TypeScriptのコンパイラくんは」
コンパイラ「
user.age
がnull
になる可能性は、関数の1行目で消えたな!」
コンパイラ「ほな、次の行ではuser.age
は確実に数値や!」
よめ太郎「ってことを理解してくれるんや」
ワイ「へえ〜、条件をつけて、先の処理に進める型を絞り込むわけか」
ワイ「でも、何でこれを型ガードって呼ぶんやろ・・・」
なぜに型「ガード」か
よめ太郎「guardって単語は」
よめ太郎「番人、見張り、みたいな意味やから」
型の番人「
number
が来るべきところにnull
が入り込まないよう、見張っておきます!」
型の番人「きちんとガードします!」
よめ太郎「↑こんなイメージや」
娘「そう言われてみると、まさに型ガードって感じだね」
ワイ「ほんまや」
ワイ「number
とかnull
ってのはあくまで今回の例やけど」
ワイ「要するに・・・」
型の番人「この型は、通ってヨシ!」
ワイ「↑こんな感じやな」
まとめ
- 来るべきでないところに
null
やundefined
が来る可能性が残っていると、TypeScriptのコンパイラはエラーを出して教えてくれる - そのとき、型アサーションで無理やりコンパイラに言うことを聞かせるのは、なるべく控えよう
- 型ガードで型を絞り込んで、あるべき型の値だけが先の処理に進めるようにすれば、ちゃんとエラーは消せる
ワイ「ってことやで!娘ちゃん」
娘「うん、聞いてたから知ってる」
ワイ「それな」
〜おしまい〜
おすすめ文献
-
型ガード - TypeScript Deep Dive 日本語版
色んな型ガードの例が紹介されてるで!