こんにちは、かつコーチです。
前回はPartial・Pick・Omitといったユーティリティ型で、既存の型を加工する方法を紹介しました。
今回は、union型(string | number のように複数の型のどれかを表す型)を安全に扱うための「型ガード」を扱います。
「union型を使うと、プロパティにアクセスするたびにエラーが出て困る」というのは、TypeScriptを触り始めた人の多くが通る道です。
型ガードとは?
union型とプロパティアクセスの壁
union型は「AかもしれないしBかもしれない」という状態を表せて便利ですが、そのままではAとB両方に共通するプロパティにしかアクセスできません。
type Dog = { kind: "dog"; bark: () => void };
type Cat = { kind: "cat"; meow: () => void };
type Pet = Dog | Cat;
function makeSound(pet: Pet): void {
pet.bark(); // エラー:Property 'bark' does not exist on type 'Cat'.
}
pet は Dog か Cat のどちらかですが、TypeScriptは実行するまでどちらか分からないため、Cat には存在しない bark の呼び出しをエラーとして弾きます。
型ガードで型を絞り込む
型ガードとは、if 文などの条件分岐によって、TypeScriptに「この中では型がAに確定している」と伝えるテクニックです。
function makeSound(pet: Pet): void {
if (pet.kind === "dog") {
pet.bark(); // OK:ここでは pet が Dog だと確定している
} else {
pet.meow(); // OK:ここでは pet が Cat だと確定している
}
}
pet.kind === "dog" という条件分岐に入った時点で、TypeScriptは「この if ブロックの中では pet は Dog だ」と自動的に絞り込んでくれます。
このように、共通のプロパティ(ここでは kind)を目印にして型を判別する手法をタグ付きユニオン(判別可能なユニオン)と呼びます。
typeofとinstanceofによる型ガード
typeofで基本型を判別する
string や number など、プリミティブ型が混在するunion型では typeof を使います。
function formatValue(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase(); // OK:string に絞り込まれている
}
return value.toFixed(2); // OK:number に絞り込まれている
}
console.log(formatValue("hello")); // HELLO
console.log(formatValue(3.14159)); // 3.14
typeof value === "string" という条件が成立したブロックでは、value は自動的に string として扱われるため、toUpperCase が安全に呼び出せます。
instanceofでクラスを判別する
クラスのインスタンスが混在する場合は instanceof を使います。
class ApiError extends Error {
statusCode: number;
constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
}
}
function handleError(error: Error | ApiError): void {
if (error instanceof ApiError) {
console.log(`APIエラー: ${error.statusCode}`); // OK:ApiError に絞り込まれている
} else {
console.log(`一般エラー: ${error.message}`);
}
}
error instanceof ApiError によって、statusCode という ApiError 固有のプロパティにも安全にアクセスできるようになります。
つまずきやすいポイント:in演算子の書き忘れ
プロパティの有無で判別しようとして詰まった話
私が実際につまずいたのは、外部APIのレスポンスをunion型で受け取る処理を書いていたときでした。
❌ Before:プロパティの有無を意識せずアクセスしてエラーになった
type SuccessResponse = { status: "success"; data: string[] };
type ErrorResponse = { status: "error"; message: string };
type ApiResult = SuccessResponse | ErrorResponse;
function handleResult(result: ApiResult): void {
console.log(result.data.length); // エラー:Property 'data' does not exist on type 'ErrorResponse'.
}
「status を見れば判別できるはず」と分かっていながら、条件分岐を書かずに result.data へ直接アクセスしてしまい、エラーメッセージの意味を理解するまで少し時間がかかりました。
status というタグ付きのプロパティがあるのに、それを判別に使っていないのがそもそもの原因でした。
✅ After:statusを条件にして型を絞り込んでからアクセスする
type SuccessResponse = { status: "success"; data: string[] };
type ErrorResponse = { status: "error"; message: string };
type ApiResult = SuccessResponse | ErrorResponse;
function handleResult(result: ApiResult): void {
if (result.status === "success") {
console.log(result.data.length); // OK:SuccessResponse に絞り込まれている
} else {
console.log(result.message); // OK:ErrorResponse に絞り込まれている
}
}
result.status === "success" という一文を条件に入れるだけで、TypeScriptは残りのプロパティの形まで正しく推論してくれます。
「union型でエラーが出たら、まず判別できる共通プロパティがないか探す」という順番で考えるようにしてから、この手のエラーで悩む時間が大幅に減りました。
in演算子でプロパティの有無を判別する
タグ用のプロパティがない型同士のunionでは、in 演算子が使えます。
type Bird = { fly: () => void };
type Fish = { swim: () => void };
type Animal = Bird | Fish;
function move(animal: Animal): void {
if ("fly" in animal) {
animal.fly(); // OK:Bird に絞り込まれている
} else {
animal.swim(); // OK:Fish に絞り込まれている
}
}
"fly" in animal は「animal が fly プロパティを持っているか」を判定し、持っていれば Bird だとTypeScriptが判断してくれます。
応用・一歩先の使い方
ユーザー定義型ガード(is構文)
判別ロジックが複雑な場合は、専用の関数として切り出せます。
type Dog = { kind: "dog"; bark: () => void };
type Cat = { kind: "cat"; meow: () => void };
type Pet = Dog | Cat;
function isDog(pet: Pet): pet is Dog {
return pet.kind === "dog";
}
function makeSound(pet: Pet): void {
if (isDog(pet)) {
pet.bark(); // OK:isDog によって Dog だと確定している
} else {
pet.meow();
}
}
戻り値の型を pet is Dog と書くのがユーザー定義型ガードの書き方で、この関数が true を返した先では pet が Dog だとTypeScriptが認識します。
判別条件が複数のプロパティにまたがる場合など、if 文の中に直接書くと読みづらくなるケースで特に効果を発揮します。
網羅性チェック(never型の活用)
union型の分岐をすべて処理しきれているかを、コンパイル時にチェックする方法もあります。
type Shape = { kind: "circle"; radius: number } | { kind: "square"; size: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
default:
const _exhaustiveCheck: never = shape; // 未対応のkindがあればここでエラーになる
return _exhaustiveCheck;
}
}
Shape に新しい種類(例えば "triangle")を追加したとき、switch に対応する case を書き忘れると default に到達し、never 型への代入エラーとしてコンパイル時に検出できます。
まとめ
この記事のポイント
- union型は、条件分岐で型を絞り込む「型ガード」を使わないと、共通しないプロパティへアクセスできない
typeofはプリミティブ型、instanceofはクラスの判別に使う- タグ付きのプロパティ(
statusやkindなど)があるunion型は、それを条件にすると綺麗に絞り込める - タグがない場合は
in演算子でプロパティの有無から判別できる - 判別ロジックが複雑なら
pet is Dogのようなユーザー定義型ガードに切り出すと読みやすい
次に読むべき記事
型ガードで安全に型を絞り込めるようになったら、次は値を固定して型推論を厳密にする「as const」を見ていきましょう。
→ 次の記事:as constとリテラル型推論