はじめに
プロダクトのソースコードで見慣れないコードを見つけました。
何をやっているのか文法から理解できなかったので先輩に聞いてみると、「型ガードで型を絞り込んでいるね」と教えていただきました。
なんのことかわからなかったので調べてみました。
わからなかったコード
前後のソースコードは省略しています。
わからなかったのは、'id' in currentMenu
という文法です。
if ('id' in currentMenu && currentMenu.id === targetId ) {
return //...省略
}
なんとなく、「currentMenuにidプロパティが存在するなら、idプロパティにアクセスする」みたいな意味かと推測しました。
試しに消してみましょう。
if ( currentMenu.id === targetId ) {
return //...省略
}
プロパティ 'id' は型 'LimitedMenu | StandardMenu' に存在しません。
プロパティ 'id' は型 'LimitedMenu' に存在しません。ts(2339)
エラーが出てしまいました😱
私:「なんだこのエラーは??????プロパティがないってどういうこと???????」
さらに、currentMenu.
まで書いてVSコードの推論を見てみたところ、確かにid
が出てきませんでした。どうやら本当にid
プロパティが存在していないようです。
何が起きているのかわからなかったので先輩に質問して、「制御フロー分析」と「型ガード」を教えていただきました。
JavaScriptのin演算子
まずは、使われているin
演算子について。
これはJavaScriptの文法で、指定したオブジェクトに特定のプロパティが存在するかどうかをチェックすることができます。
プロパティが存在する場合はtrue、存在しないならfalseを返します。
const user = { name: 'odeko', id: '@odendayoko' };
console.log('id' in user);
// Expected output: true
詳細はこちらをご覧ください。
よって、'id' in currentMenu
と書いた場合、currentMenu
にin
プロパティが存在すればtrue、存在しない場合はfalseと判定されます。
制御フロー分析(Control Flow Analysis, CFA)
ここからはTypeScriptのお話です。
制御フロー分析という機能があるらしいです。
TypeScriptはifやforループなどの制御フローを分析することで、コードが実行されるタイミングでの型の可能性を判断しています。
下記の例で考えてみます。
monthの型はstring | number
です。if文の分岐に入るまでは、monthがstring型かnumber型のどちらかが決まっていません。この場合、string型でしか使えないメソッドへアクセスしようとするとTypeScriptのエラーが出ます。
そこで、if文でmonthの型をstring型に確定させるように条件分岐させます。これにより、メソッド実行時の型がstringに決まるので、エラーは発生しません。
const showMonth = (month: string | number) => {
// ここでメソッドにアクセスすると、string型かnumber型か決まらないのでエラーになる
// console.log(month.padStart(2, "0"))
if (typeof month === "string") {
// string型に決まるのでエラーは起きない
console.log(month.padStart(2, "0"));
}
}
こんな感じで、TypeScriptはコードの特定の部分で変数がどの型であるかを推論してくれるみたいです。
型ガード
上記の例で、if(typeof month === "string")
という条件分岐で変数の型を判定して型の絞り込みを行っています。このような型の制御を型ガードと呼ぶらしいです。
代表的なのはtypeof演算子
を使った型ガードです。
他にも色々種類があるようなので詳しくはこちらをご覧ください。
今回は、in演算子
を使って型を絞り込んでいたことがわかりました。
コードを読み解いてみる
ではわからなかったコードに戻ってみましょう。
interface LimitedMenu {
id: number
name: string
url: string
}
interface StandardMenu {
name: string
url: string
}
// currentPhoto: LimitedMenu | StandardMenu
if ('id' in currentMenu && currentMenu.id === targetId ) {
return //...省略
}
currentMenu
の型はLimitedMenu | StandardMenu
です。型を確認すると、LimitedMenu
にはid
プロパティが存在しますが、StandardMenu
には存在しません。
よって、'id' in currentMenu &&
とすることで、右項ではcurrentMenu
の型がLimitedMenu
に決まります。LimitedMenu
型なら、id
プロパティが存在するのでアクセスしてもエラーは起きません。
currentMenu.id
とした場合にエラーが発生したのは、currentMenu
がid
プロパティを持たないStandardMenu
型をとる可能性があるからです。型として存在しないプロパティにアクセスすることはできません。
終わりに
簡単そうに見えて意外と奥が深い内容でした!
ユニオン型で、各要素の型のプロパティに違いがある場合は注意が必要ですね💡