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

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

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型であることを条件判定を追加することでmonthpadStartメソッドの実行時は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"));
}
}

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

次の例ではmonthtoFixedメソッドの呼び出しは条件分岐のスコープ外であり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());
}

この変更によりエラーとなっていたmonthtoFixedメソッドの呼び出しの型エラーが解消されます。

これは制御フロー分析によりmonth変数がstring型の場合は早期リターンにより関数が終了し、monthtoFixedメソッドが実行されるタイミングでは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);
'date' is possibly 'null'.18047'date' is possibly 'null'.
}
}
ts
function getMonth(date: string | Date | null) {
if (typeof date === "object") {
console.log(date.getMonth() + 1);
'date' is possibly 'null'.18047'date' 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 Swordsman {
slashSword(): void;
}
 
function attack(player: Wizard | Swordsman) {
if ("castMagic" in player) {
player.castMagic();
} else {
player.slashSword();
}
}
ts
interface Wizard {
castMagic(): void;
}
interface Swordsman {
slashSword(): void;
}
 
function attack(player: Wizard | Swordsman) {
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 | Swordsman) {
if (isWizard(player)) {
player.castMagic();
} else {
player.slashSword();
}
}
ts
function isWizard(player: Player): player is Wizard {
return "castMagic" in player;
}
function attack(player: Wizard | Swordsman) {
if (isWizard(player)) {
player.castMagic();
} else {
player.slashSword();
}
}

この名称(user-defined type guard)は英語としても長いらしく、型ガード関数(type guarding function, guard's function)と呼ばれることもあります。

📄️ 型ガード関数

TypeScriptのコンパイラはifやswitchといった制御フローの各場所での変数の型を分析しており、この機能を制御フロー分析(control flow analysis)と呼びます。

型ガードの変数代入

型ガードに変数を使うこともできます。

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);
}
}

switch (true) による型の絞り込み

switch文はcase節の値によって異なるコードを実行します。通常、case節には文字列や数値を指定しますが、TypeScriptではswitch (true)を使用すると、各case節で真偽値を返す式を評価できます。trueと評価されたcaseブロック内では、その条件に基づいて型が自動的に絞り込まれます。

ts
function handleValue(value: string | number | boolean): void {
switch (true) {
case typeof value === "string":
console.log(`String value: ${value.padStart(2, "0")}`);
break;
case typeof value === "number":
console.log(`Number value: ${value.toFixed(2)}`);
break;
case typeof value === "boolean":
console.log(`Boolean value: ${value}`);
break;
default:
console.log("Unknown type");
}
}
ts
function handleValue(value: string | number | boolean): void {
switch (true) {
case typeof value === "string":
console.log(`String value: ${value.padStart(2, "0")}`);
break;
case typeof value === "number":
console.log(`Number value: ${value.toFixed(2)}`);
break;
case typeof value === "boolean":
console.log(`Boolean value: ${value}`);
break;
default:
console.log("Unknown type");
}
}

typeofに限らずinstanceofに対しても同様に使用することができます。次の例のUserErrorSystemErrorは独自にusersystemプロパティを持っているクラスです。switch (true)を使用してどちらのエラーかを判別し、それぞれのプロパティにアクセスしています。

ts
function handleError(error: UserError | SystemError): void {
switch (true) {
case error instanceof UserError:
console.log(`User error for ${error.user}: ${error.message}`);
break;
case error instanceof SystemError:
console.log(`System error for ${error.system}: ${error.message}`);
break;
default:
console.log("Unknown error type");
}
}
ts
function handleError(error: UserError | SystemError): void {
switch (true) {
case error instanceof UserError:
console.log(`User error for ${error.user}: ${error.message}`);
break;
case error instanceof SystemError:
console.log(`System error for ${error.system}: ${error.message}`);
break;
default:
console.log("Unknown error type");
}
}

ユーザー定義型ガード関数をswitch (true)に使用することもできます。

ts
function handleValue(value: Panda | Broccoli | User): void {
switch (true) {
case isPanda(value):
console.log(`I am a panda: ${value.panda}`);
break;
case isBroccoli(value):
console.log(`I am broccoli: ${value.broccoli}`);
break;
case isUser(value):
console.log(`I am ${value.name}`);
break;
}
}
ts
function handleValue(value: Panda | Broccoli | User): void {
switch (true) {
case isPanda(value):
console.log(`I am a panda: ${value.panda}`);
break;
case isBroccoli(value):
console.log(`I am broccoli: ${value.broccoli}`);
break;
case isUser(value):
console.log(`I am ${value.name}`);
break;
}
}

関連情報

📄️ any型

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

📄️ anyとunknownの違い

any, unknown型はどのような値も代入できます。