【TypeScript】型ガードでunion型を安全に絞り込む

JavaScript

こんにちは、かつコーチです。

前回は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'.
}

petDogCat のどちらかですが、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 ブロックの中では petDog だ」と自動的に絞り込んでくれます。

このように、共通のプロパティ(ここでは kind)を目印にして型を判別する手法をタグ付きユニオン(判別可能なユニオン)と呼びます。

typeofとinstanceofによる型ガード

typeofで基本型を判別する

stringnumber など、プリミティブ型が混在する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 は「animalfly プロパティを持っているか」を判定し、持っていれば 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 を返した先では petDog だと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 はクラスの判別に使う
  • タグ付きのプロパティ(statuskind など)があるunion型は、それを条件にすると綺麗に絞り込める
  • タグがない場合は in 演算子でプロパティの有無から判別できる
  • 判別ロジックが複雑なら pet is Dog のようなユーザー定義型ガードに切り出すと読みやすい

次に読むべき記事

型ガードで安全に型を絞り込めるようになったら、次は値を固定して型推論を厳密にする「as const」を見ていきましょう。

→ 次の記事:as constとリテラル型推論

タイトルとURLをコピーしました