型ガードの書き方と落とし穴

型ガードは、実行時の値チェックを型システムに伝える仕組みだ。unknown や ユニオン型を安全に扱うために欠かせないが、書き方を間違えると型安全性が崩壊する。正しいパターンと落とし穴を押さえておこう。

組み込みの型ガード

TypeScript は typeofinstanceofin などの演算子を型ガードとして認識する。

function process(value: string | number) {
  if (typeof value === "string") {
    // value は string 型に絞り込まれる
    console.log(value.toUpperCase());
  } else {
    // value は number 型に絞り込まれる
    console.log(value.toFixed(2));
  }
}

instanceof はクラスのインスタンスを判定する。

function handleError(error: unknown) {
  if (error instanceof Error) {
    console.log(error.message);
  } else {
    console.log(String(error));
  }
}

in 演算子はプロパティの存在をチェックする。

type Dog = { bark: () => void };
type Cat = { meow: () => void };

function speak(animal: Dog | Cat) {
  if ("bark" in animal) {
    animal.bark();
  } else {
    animal.meow();
  }
}

ユーザー定義型ガード

組み込みの型ガードでは対応できない場合、自分で型ガード関数を書く。

interface User {
  name: string;
  email: string;
}

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "name" in value &&
    "email" in value &&
    typeof (value as User).name === "string" &&
    typeof (value as User).email === "string"
  );
}

戻り値の型 value is User がポイントだ。これを「型述語(type predicate)」と呼ぶ。関数が true を返したとき、引数の型が User に絞り込まれることを TypeScript に伝えている。

const data: unknown = JSON.parse('{"name": "Alice", "email": "alice@example.com"}');

if (isUser(data)) {
  // data は User 型として扱える
  console.log(data.name);
}

落とし穴:型ガードの嘘

型ガード関数は嘘をつける。TypeScript は関数の中身を検証しない。

// 危険な型ガード
function isUser(value: unknown): value is User {
  return true;  // 常に true を返す
}

const data: unknown = { foo: "bar" };

if (isUser(data)) {
  // コンパイルは通るが、実行時にエラー
  console.log(data.email.toUpperCase());
}
型ガードが正しい場合

実行時チェックと型述語が一致している。安全。

型ガードが嘘をついている場合

実行時チェックが不十分なのに型述語だけ書いている。実行時エラーの原因。

落とし穴:プロパティの型チェック漏れ

プロパティの「存在」だけでなく「型」もチェックしないと危険だ。

// 不十分な型ガード
function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "name" in value &&
    "email" in value
    // name と email が string かどうかチェックしていない
  );
}

const data = { name: 123, email: null };

if (isUser(data)) {
  // data.name は number、data.email は null
  console.log(data.name.toUpperCase());  // 実行時エラー
}

asserts を使った型ガード

TypeScript 3.7 以降、asserts キーワードでアサーション関数を書ける。

function assertIsUser(value: unknown): asserts value is User {
  if (
    typeof value !== "object" ||
    value === null ||
    !("name" in value) ||
    !("email" in value)
  ) {
    throw new Error("Not a User");
  }
}

const data: unknown = fetchData();
assertIsUser(data);
// ここ以降、data は User 型
console.log(data.name);

asserts 関数は戻り値を返さず、検証に失敗したら例外を投げる。これにより if 文なしで型を絞り込める。

実践的なパターン

Zod や Valibot を使う

型ガードを手書きするのは面倒でミスしやすい。スキーマ検証ライブラリを使えば、型定義と実行時チェックを一元管理できる。

配列のフィルタリング

filter メソッドに型ガードを渡すと、配列の型も絞り込まれる。(arr as User[]).filter(...) のようなキャストを避けられる。

const items: (User | null)[] = [user1, null, user2];

// null を除外し、型も User[] になる
const users = items.filter((item): item is User => item !== null);

型ガードは TypeScript の型安全性を支える重要な機能だが、責任は開発者にある。型述語が嘘をつかないよう、実行時チェックは慎重に書こう。