TypeScript 速習チュートリアル

TypeScript 型ガード

TypeScriptのType Guards(型ガード)は、特定のスコープ内で変数の型を絞り込む(Narrowing)ための強力な機能です。
これらを利用することで、実行時に変数の具体的な型を判定し、TypeScriptコンパイラに対してその型安全性を保証・強制できるようになります。

1. なぜ Type Guards を使うのか?

  • 型の安全性(Type Safety): 適切な型に対してのみ操作が行われることを保証します。
  • コードの明瞭性(Code Clarity): 型チェックが明示的になり、自己文書化されたコードになります。
  • 開発ツールのサポート向上(Better Tooling): 正確なIntelliSenseやコード補完が得られます。
  • エラーの防止(Error Prevention): コンパイル時に型関連のエラーをキャッチできます。
  • ランタイムの安全性(Runtime Safety): 実行時にもう一層の型チェックレイヤーを追加できます。

2. Type Guard のパターン

  • typeof 型ガード
  • instanceof 型ガード
  • 型述語(Type Predicates)を用いたユーザー定義型ガード
  • リテラル型を用いた識別子付き共用体(Discriminated Unions)
  • in 演算子による型ガード
  • 型アサーション関数(Type assertion functions)

3. typeof 型ガード

typeof 演算子は、プリミティブ値の型を実行時にチェックするための組み込み型ガードです。
文字列、数値、真偽値などのプリミティブ型を絞り込む際に非常に便利です。

3.1 基本的な使い方

条件分岐の中で typeof チェックを使用し、プリミティブの共用体型を絞り込みます。

コード例

// typeof を使ったシンプルな型ガード
function formatValue(value: string | number): string {
  if (typeof value === 'string') {
    // ここで TypeScript は value が string 型であることを認識します
    return value.trim().toUpperCase();
  } else {
    // ここで TypeScript は value が number 型であることを認識します
    return value.toFixed(2);
  }
}

// 使用例
const result1 = formatValue('  hello  ');  // "HELLO"
const result2 = formatValue(42.1234);      // "42.12"

上記の例では、TypeScriptは if 文の各ブランチにおいて value の型を正しく理解しています。

4. instanceof 型ガード

instanceof 演算子は、オブジェクトが特定のクラスまたはコンストラクタ関数のインスタンスであるかどうかをチェックします。
カスタムクラスや組み込みオブジェクトの型を絞り込む際に有用です。

4.1 クラスベースの型ガード

instanceof でコンストラクタを確認することで、クラスインスタンスの共用体型を絞り込みます。

コード例

class Bird {
  fly() {
    console.log("空を飛んでいます...");
  }
}

class Fish {
  swim() {
    console.log("泳いでいます...");
  }
}

function move(animal: Bird | Fish) {
  if (animal instanceof Bird) {
    // ここで TypeScript は animal が Bird インスタンスであることを認識します
    animal.fly();
  } else {
    // ここで TypeScript は animal が Fish インスタンスであることを認識します
    animal.swim();
  }
}

5. ユーザー定義型ガード

より複雑な型チェックを行う場合、型述語(Type Predicates)を使用してカスタムの型ガード関数を作成できます。
これらは、引数名 is 型 という形式の型述語を返す関数です。

5.1 型述語関数

value is Type のような述語を返すことで、関数が true を返したブランチにおいて TypeScript が型を絞り込めるようにします。

コード例

interface Car {
  make: string;
  model: string;
  year: number;
}

interface Motorcycle {
  make: string;
  model: string;
  year: number;
  type: "sport" | "cruiser";
}

// ユーザー定義の型ガード関数(型述語を使用)
function isCar(vehicle: Car | Motorcycle): vehicle is Car {
  return (vehicle as Motorcycle).type === undefined;
}

function displayVehicleInfo(vehicle: Car | Motorcycle) {
  console.log(`メーカー: ${vehicle.make}, モデル: ${vehicle.model}, 年式: ${vehicle.year}`);

  if (isCar(vehicle)) {
    // ここで TypeScript は vehicle が Car 型であることを認識します
    console.log("これは乗用車です");
  } else {
    // ここで TypeScript は vehicle が Motorcycle 型であることを認識します
    console.log(`これは ${vehicle.type} タイプのバイクです`);
  }
}

関数シグネチャの vehicle is Car が型述語であり、関数が true を返した時に型を絞り込むよう TypeScript に指示します。

6. 識別子付き共用体(Discriminated Unions)

識別子付き共用体(タグ付き共用体とも呼ばれます)は、共通のプロパティ(識別子)を使用して、共用体内の異なるオブジェクト型を区別するパターンです。
このパターンは、型ガードと組み合わせることで非常に強力になります。

6.1 基本的な識別子付き共用体

共通のリテラルプロパティ(例: kind)を使用して、switch 文などで正確なバリアントに絞り込みます。

コード例

interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}

type Shape = Circle | Square;

function calculateArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      // ここで TypeScript は shape が Circle 型であることを認識します
      return Math.PI * shape.radius ** 2;
    case "square":
      // ここで TypeScript は shape が Square 型であることを認識します
      return shape.sideLength ** 2;
  }
}

kind プロパティが識別子(Discriminant)として機能し、図形の型を特定するために使用されています。

7. in 演算子

in 演算子は、オブジェクトに特定のプロパティが存在するかどうかをチェックします。
異なる型がそれぞれ固有のプロパティを持っている共用体型を絞り込む際に特に役立ちます。

7.1 プロパティ存在チェック

特徴的なプロパティが存在するかをテストすることで、共用体のメンバーを絞り込みます。

コード例

interface Dog {
  bark(): void;
}

interface Cat {
  meow(): void;
}

function makeSound(animal: Dog | Cat) {
  if ("bark" in animal) {
    // ここで TypeScript は animal が Dog 型であることを認識します
    animal.bark();
  } else {
    // ここで TypeScript は animal が Cat 型であることを認識します
    animal.meow();
  }
}

8. 型アサーション関数

型アサーション関数は、型のアサーションに失敗した場合にエラーをスローする特殊な型ガードです。
ランタイムでのデータバリデーションに役立ちます。

8.1 アサーション関数

型の絞り込みを行い、無効な入力に対して例外をスローするランタイムチェックを実装します。

コード例

// 型アサーション関数
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error('値が文字列ではありません');
  }
}

// カスタムエラーを伴うアサーション関数
function assert(condition: unknown, message: string): asserts condition {
  if (!condition) {
    throw new Error(message);
  }
}

// 使用例
function processInput(input: unknown) {
  assertIsString(input);
  // これ以降、input は string 型として扱われます
  console.log(input.toUpperCase());
}

// カスタムエラーの使用
function processNumber(value: unknown): number {
  assert(typeof value === 'number', '値は数値である必要があります');
  // これ以降、value は number 型として扱われます
  return (value as number) * 2;
}

9. ベストプラクティス

9.1 各型ガードの使い分け

  • プリミティブ型(string, number, boolean など)には typeof を使用する。
  • クラスインスタンスや組み込みオブジェクトには instanceof を使用する。
  • 複雑なバリデーションロジックには ユーザー定義型ガード を作成する。
  • 共通の識別子を持つ関連型には 識別子付き共用体 を使用する。
  • プロパティの存在を確認する場合には in 演算子 を使用する。
  • エラーを伴うランタイムバリデーションには 型アサーション関数 を使用する。

9.2 パフォーマンスに関する考慮事項

  • typeofinstanceof は非常に高速です。
  • パフォーマンスがクリティカルな箇所では、ユーザー定義型ガードの中に複雑すぎるロジックを入れないようにしましょう。
  • 負荷の高いチェックを複数回行う場合は、型述語を適切に利用して効率化を検討してください。