メインコンテンツまでスキップ

制御フロー分析と型ガードによる型の絞り込み

TypeScriptは制御フローと型ガードにより、処理の流れに応じて変数の型を絞り込むことができます。

ユニオン型と曖昧さ

ユニオン型で変数の型注釈を書いた時に、片方の型でしか定義されていないメソッドやプロパティにアクセスをすると型エラーが発生します。

ts
function showMonth(month: string | number) {
console.log(month.padStart(2, "0"));
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'.
}
ts
function showMonth(month: string | number) {
console.log(month.padStart(2, "0"));
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'.
}

これはmonthの変数がstringornumber型のどちらかになる可能性がありnumber型が渡された時に未定義なメソッドへのアクセスが発生する危険があるためです。

制御フロー分析

TypeScriptはifforループなどの制御フローを分析することで、コードが実行されるタイミングでの型の可能性を判断しています。

先ほどの例にmonth変数がstring型であることを条件判定を追加することでmonth.padStart()の実行時はmonthstring型であるとTypeScriptが判断し型エラーを解消することができます。

ts
function showMonth(month: string | number) {
if (typeof month === "string") {
console.log(month.padStart(2, "0"));
}
}
ts
function showMonth(month: string | number) {
if (typeof month === "string") {
console.log(month.padStart(2, "0"));
}
}

もう少し複雑な例を見てみましょう。

次の例ではmonth.toFixed()の呼び出しは条件分岐のスコープ外でありmonth変数の型がstring | numberとなるため型エラーが発生します。

ts
function showMonth(month: string | number) {
if (typeof month === "string") {
console.log(month.padStart(2, "0"));
}
console.log(month.toFixed());
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'.
}
ts
function showMonth(month: string | number) {
if (typeof month === "string") {
console.log(month.padStart(2, "0"));
}
console.log(month.toFixed());
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'.
}

この関数の最初の条件分岐の中にreturnを追記して早期リターンで関数の処理を終了させてみます。

ts
function showMonth(month: string | number) {
if (typeof month === "string") {
console.log(month.padStart(2, "0"));
return;
}
console.log(month.toFixed());
}
ts
function showMonth(month: string | number) {
if (typeof month === "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演算子では値の型を調べることができます。

次の例ではtypeofmonth変数の型をstring型と判定しています。

ts
function showMonth(month: string | number) {
if (typeof month === "string") {
console.log(month.padStart(2, "0"));
}
}
ts
function showMonth(month: string | number) {
if (typeof month === "string") {
console.log(month.padStart(2, "0"));
}
}

typeofの型ガードではtypeof null === "object"となる点に注意が必要です。

JavaScriptにおいてnullはオブジェクトであるため、次の型ガードを書いた場合はdate変数はDate | nullに絞り込まれnullとなる可能性が残ってしまい型エラーが発生します。

ts
function getMonth(date: string | Date | null) {
if (typeof date === "object") {
console.log(date.getMonth() + 1);
Object is possibly 'null'.2531Object is possibly 'null'.
}
}
ts
function getMonth(date: string | Date | null) {
if (typeof date === "object") {
console.log(date.getMonth() + 1);
Object is possibly 'null'.2531Object is possibly 'null'.
}
}

date != nullの型ガードを追加することで型エラーを解消できます。

ts
function getMonth(date: string | Date | null) {
if (typeof date === "object" && date != null) {
console.log(date.getMonth() + 1);
}
}
ts
function getMonth(date: string | Date | null) {
if (typeof date === "object" && date != null) {
console.log(date.getMonth() + 1);
}
}

instanceof

typeofでインスタンスを判定した場合はオブジェクトであることまでしか判定ができません。
特定のクラスのインスタンスであることを判定する型ガードを書きたい場合はinstanceofを利用します。

ts
function getMonth(date: string | Date) {
if (date instanceof Date) {
console.log(date.getMonth() + 1);
}
}
ts
function getMonth(date: string | Date) {
if (date instanceof Date) {
console.log(date.getMonth() + 1);
}
}

in

特定のクラスのインスタンスであることを明示せず、in演算子でオブジェクトが特定のプロパティを持つかを判定する型ガードを書くことで型を絞り込むこともできます。

ts
interface Wizard {
castMagic(): void;
}
interface SwordMan {
slashSword(): void;
}
 
function attack(player: Wizard | SwordMan) {
if ("castMagic" in player) {
player.castMagic();
} else {
player.slashSword();
}
}
ts
interface Wizard {
castMagic(): void;
}
interface SwordMan {
slashSword(): void;
}
 
function attack(player: Wizard | SwordMan) {
if ("castMagic" in 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();
}
}
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();
}
}

📄️ 型ガード関数

Type predicateの宣言は戻り値がboolean型の関数に対して適用でき、戻り値の型の部分を次のように書き替えます。

型ガードの変数代入

型ガードに変数を使うこともできます。
ただし、この文法は TypeScript4.4 以降のみで有効なため、使用する場合はバージョンに注意してください。

ts
function getMonth(date: string | Date) {
const isDate = date instanceof Date;
if (isDate) {
console.log(date.getMonth() + 1);
}
}
ts
function getMonth(date: string | Date) {
const isDate = date instanceof Date;
if (isDate) {
console.log(date.getMonth() + 1);
}
}

関連情報

📄️ any型

TypeScriptのany型は、どんな型でも代入を許す型です。プリミティブ型であれオブジェクトであれ何を代入してもエラーになりません。

📄️ anyとunknownの違い

any, unknown型はどのような値も代入できます。
  • 質問する ─ 読んでも分からなかったこと、TypeScriptで分からないこと、お気軽にGitHubまで🙂
  • 問題を報告する ─ 文章やサンプルコードなどの誤植はお知らせください。