制御フロー分析と型ガードによる型の絞り込み
TypeScriptは制御フローと型ガードにより、処理の流れに応じて変数の型を絞り込むことができます。
ユニオン型と曖昧さ
ユニオン型で変数の型注釈を書いた時に、片方の型でしか定義されていないメソッドやプロパティにアクセスをすると型エラーが発生します。
ts
functionshowMonth (month : string | number) {Property 'padStart' does not exist on type 'string | number'. Property 'padStart' does not exist on type 'number'.2339Property 'padStart' does not exist on type 'string | number'. Property 'padStart' does not exist on type 'number'.console .log (month .(2, "0")); padStart }
ts
functionshowMonth (month : string | number) {Property 'padStart' does not exist on type 'string | number'. Property 'padStart' does not exist on type 'number'.2339Property 'padStart' does not exist on type 'string | number'. Property 'padStart' does not exist on type 'number'.console .log (month .(2, "0")); padStart }
これはmonth
の変数がstring
ornumber
型のどちらかになる可能性がありnumber
型が渡された時に未定義なメソッドへのアクセスが発生する危険があるためです。
制御フロー分析
TypeScriptはif
やfor
ループなどの制御フローを分析することで、コードが実行されるタイミングでの型の可能性を判断しています。
先ほどの例にmonth
変数がstring
型であることを条件判定を追加することでmonth
のpadStart
メソッドの実行時はmonth
がstring
型であるとTypeScriptが判断し型エラーを解消することができます。
ts
functionshowMonth (month : string | number) {if (typeofmonth === "string") {console .log (month .padStart (2, "0"));}}
ts
functionshowMonth (month : string | number) {if (typeofmonth === "string") {console .log (month .padStart (2, "0"));}}
もう少し複雑な例を見てみましょう。
次の例ではmonth
のtoFixed
メソッドの呼び出しは条件分岐のスコープ外でありmonth
変数の型がstring | number
となるため型エラーが発生します。
ts
functionshowMonth (month : string | number) {if (typeofmonth === "string") {console .log (month .padStart (2, "0"));}Property 'toFixed' does not exist on type 'string | number'. Property 'toFixed' does not exist on type 'string'.2339Property 'toFixed' does not exist on type 'string | number'. Property 'toFixed' does not exist on type 'string'.console .log (month .()); toFixed }
ts
functionshowMonth (month : string | number) {if (typeofmonth === "string") {console .log (month .padStart (2, "0"));}Property 'toFixed' does not exist on type 'string | number'. Property 'toFixed' does not exist on type 'string'.2339Property 'toFixed' does not exist on type 'string | number'. Property 'toFixed' does not exist on type 'string'.console .log (month .()); toFixed }
この関数の最初の条件分岐の中にreturn
を追記して早期リターンで関数の処理を終了させてみます。
ts
functionshowMonth (month : string | number) {if (typeofmonth === "string") {console .log (month .padStart (2, "0"));return;}console .log (month .toFixed ());}
ts
functionshowMonth (month : string | number) {if (typeofmonth === "string") {console .log (month .padStart (2, "0"));return;}console .log (month .toFixed ());}
この変更によりエラーとなっていたmonth
のtoFixed
メソッドの呼び出しの型エラーが解消されます。
これは制御フロー分析によりmonth
変数がstring
型の場合は早期リターンにより関数が終了し、month
のtoFixed
メソッドが実行されるタイミングではmonth
変数はnumber
型のみであるとTypeScriptが判断するためです。
型ガード
制御フローの説明において、型の曖昧さを回避するためにif(typeof month === "string")
という条件判定で変数の型を判定して型の絞り込みを行いました。
このような型チェックのコードを型ガードと呼びます。
typeof
代表的な例はtypeof
演算子を利用した型ガードです。
📄️ typeof演算子
JavaScriptのtypeof演算子では値の型を調べることができます。
次の例ではtypeof
でmonth
変数の型をstring
型と判定しています。
ts
functionshowMonth (month : string | number) {if (typeofmonth === "string") {console .log (month .padStart (2, "0"));}}
ts
functionshowMonth (month : string | number) {if (typeofmonth === "string") {console .log (month .padStart (2, "0"));}}
typeof
の型ガードではtypeof null === "object"
となる点に注意が必要です。
JavaScriptにおいてnull
はオブジェクトであるため、次の型ガードを書いた場合はdate
変数はDate | null
に絞り込まれnull
となる可能性が残ってしまい型エラーが発生します。
ts
functiongetMonth (date : string |Date | null) {if (typeofdate === "object") {'date' is possibly 'null'.18047'date' is possibly 'null'.console .log (. date getMonth () + 1);}}
ts
functiongetMonth (date : string |Date | null) {if (typeofdate === "object") {'date' is possibly 'null'.18047'date' is possibly 'null'.console .log (. date getMonth () + 1);}}
date != null
の型ガードを追加することで型エラーを解消できます。
ts
functiongetMonth (date : string |Date | null) {if (typeofdate === "object" &&date != null) {console .log (date .getMonth () + 1);}}
ts
functiongetMonth (date : string |Date | null) {if (typeofdate === "object" &&date != null) {console .log (date .getMonth () + 1);}}
instanceof
typeof
でインスタンスを判定した場合はオブジェクトであることまでしか判定ができません。
特定のクラスのインスタンスであることを判定する型ガードを書きたい場合はinstanceof
を利用します。
ts
functiongetMonth (date : string |Date ) {if (date instanceofDate ) {console .log (date .getMonth () + 1);}}
ts
functiongetMonth (date : string |Date ) {if (date instanceofDate ) {console .log (date .getMonth () + 1);}}
in
特定のクラスのインスタンスであることを明示せず、in
演算子でオブジェクトが特定のプロパティを持つかを判定する型ガードを書くことで型を絞り込むこともできます。
ts
interfaceWizard {castMagic (): void;}interfaceSwordMan {slashSword (): void;}functionattack (player :Wizard |SwordMan ) {if ("castMagic" inplayer ) {player .castMagic ();} else {player .slashSword ();}}
ts
interfaceWizard {castMagic (): void;}interfaceSwordMan {slashSword (): void;}functionattack (player :Wizard |SwordMan ) {if ("castMagic" inplayer ) {player .castMagic ();} else {player .slashSword ();}}
ユーザー定義の型ガード関数
型ガードはインラインで記述する以外にも関数として定義することもできます。
ts
function isWizard(player: Player): player is Wizard {return "castMagic" in player;}function attack(player: Wizard | SwordMan) {if (isWizard(player)) {player.castMagic();} else {player.slashSword();}}
ts
function isWizard(player: Player): player is Wizard {return "castMagic" in player;}function attack(player: Wizard | SwordMan) {if (isWizard(player)) {player.castMagic();} else {player.slashSword();}}
この名称(user-defined type guard)は英語としても長いらしく、型ガード関数(type guarding function, guard's function)と呼ばれることもあります。
📄️ 型ガード関数
TypeScriptのコンパイラはifやswitchといった制御フローの各場所での変数の型を分析しており、この機能を制御フロー分析(control flow analysis)と呼びます。
型ガードの変数代入
型ガードに変数を使うこともできます。
ただし、この文法は TypeScript4.4 以降のみで有効なため、使用する場合はバージョンに注意してください。
ts
functiongetMonth (date : string |Date ) {constisDate =date instanceofDate ;if (isDate ) {console .log (date .getMonth () + 1);}}
ts
functiongetMonth (date : string |Date ) {constisDate =date instanceofDate ;if (isDate ) {console .log (date .getMonth () + 1);}}
関連情報
📄️ any型
TypeScriptのany型は、どんな型でも代入を許す型です。プリミティブ型であれオブジェクトであれ何を代入してもエラーになりません。
📄️ anyとunknownの違い
any, unknown型はどのような値も代入できます。