TypeScript型ガード完全ガイド〜typeof・instanceof・in・is・discriminated unionと網羅性チェック【2026年版】〜

TypeScriptで開発をしていて、「string | numberのようなユニオン型を絞り込んで使う」「unknownで受け取ったAPIレスポンスを安全に扱いたい」という場面は日常的に発生します。このときに必要なのが型ガード(Type Guard)narrowing(絞り込み)の知識です。

本記事はTypeScript 5.x準拠で、typeof / instanceof / in / user-defined type guard (is) / asserts / discriminated union / 網羅性チェックまで、コピペで動く40本超のコードサンプルで網羅的に解説します。Zod・valibot・io-tsとの実戦連携、Branded Types、Reactでの活用、アンチパターンまで含めて、現役Webエンジニアの実務で必要な知識をすべて詰め込みました。

この記事で得られること

  • typeof / instanceof / in 演算子による絞り込みの動作仕様
  • user-defined type guard (x is T) と asserts の使い分け
  • discriminated union と never 網羅性チェックの実装パターン
  • Zod / valibot / io-ts によるランタイム検証との連携
  • 40本以上のコピペ可能TS 5.xコードサンプル(API・React・Branded Types)
  1. 型ガードとnarrowingの全体像
    1. なぜ型ガードが必要なのか
    2. 本記事で扱う型ガード手法一覧
  2. typeof による narrowing
    1. typeofで判定できる値の種類
    2. 基本: string/number 判定
    3. 3つ以上のユニオンを絞り込む
    4. typeof ‘function’ で関数を判定
    5. typeof ‘object’ は要注意(nullと配列)
  3. instanceof による narrowing
    1. Errorクラスの絞り込み
    2. 独自エラークラスの階層分岐
    3. Date型の絞り込み
    4. クラス階層での narrowing
  4. in 演算子による narrowing
    1. 基本: プロパティの有無で分岐
    2. 3種類以上のユニオンを in で分岐
    3. in でoptional プロパティを扱うときの落とし穴
  5. user-defined type guard (x is T)
    1. 基本: シンプルなis関数
    2. unknownからの型ガード
    3. 配列の型ガード
    4. ジェネリックな型ガード
  6. discriminated union と網羅性チェック
    1. 基本: tagプロパティで分岐
    2. 網羅性チェック(never)
    3. 新しい状態を追加するとコンパイルエラー
    4. Reduxスタイルのアクション分岐
    5. tagged union vs discriminated union
  7. asserts キーワードによるアサーション関数
    1. 基本: asserts value is T
    2. 非null アサーション関数
    3. 条件式そのものをassertする
  8. unknown と JSONパースの安全な扱い
    1. any と unknown の違い
    2. JSONパース結果を安全に扱う
    3. fetch+型ガードの定石パターン
  9. その他の narrowing パターン
    1. Equality narrowing(等価比較で絞り込み)
    2. Truthiness narrowing(真偽値で絞り込み)
    3. 0や空文字の落とし穴
    4. Nullish coalescing と narrowing
    5. Array.isArray による narrowing
    6. 制御フロー解析(Control Flow Analysis)の挙動
    7. typeof + keyof の組み合わせ
  10. Branded Types と型ガード
    1. Branded Type の作り方
    2. Branded Type + type guard
    3. Email型・URL型などの応用
  11. enum と literal union での型ガード
    1. literal union(推奨)
    2. enum を使う場合の型ガード
  12. Zod / valibot / io-ts によるランタイム検証連携
    1. Zod(最も人気)
    2. Zod + 自作 type guard
    3. valibot(軽量・モジュラー)
    4. io-ts(関数型風)
    5. 3ライブラリの選び方
  13. React Props と APIレスポンスでの実戦活用
    1. React Props の discriminated union
    2. React useReducer + discriminated union
    3. APIレスポンスの早期return パターン
  14. アンチパターンとリファクタリング
    1. アンチパターン1: as any による絞り込み
    2. アンチパターン2: 二重キャスト(as unknown as T)
    3. アンチパターン3: 過剰な non-null アサーション(!)
    4. アンチパターン4: 型ガード関数が偽陽性を返す
    5. Before/After: ネストしたif地獄をdiscriminated unionに
    6. Before/After: 文字列比較の連鎖をswitchに
  15. まとめ:型ガードを使いこなすための指針
    1. 使い分けのチートシート
    2. 関連記事

型ガードとnarrowingの全体像

型ガードとは、TypeScriptコンパイラに対して「この変数はこの型である」とコード上で証明するための仕組みです。コンパイラはコードフロー解析(Control Flow Analysis)によって、ガード判定のtrue/falseが確定したスコープ内で型を自動的に絞り込みます。

なぜ型ガードが必要なのか

ユニオン型をそのまま使うと、両方の型に共通するプロパティしかアクセスできません。下の例ではエラーになります。

// ❌ ユニオン型のまま使うとエラー
function printLength(value: string | number) {
  console.log(value.length); // Error: Property 'length' does not exist on type 'string | number'
}

これをtypeofで絞り込むと、各分岐内で個別の型として扱えます。

// ✅ 型ガードで絞り込んでから使う
function printLength(value: string | number) {
  if (typeof value === "string") {
    console.log(value.length); // OK: ここではvalueはstring
  } else {
    console.log(value.toFixed(2)); // OK: ここではvalueはnumber
  }
}

本記事で扱う型ガード手法一覧

手法 主用途 対象
typeof プリミティブ型の判定 string/number/boolean等
instanceof クラスインスタンスの判定 Date/Error/独自クラス
in プロパティの有無で判定 オブジェクト型
user-defined (is) 独自判定関数 任意の構造
asserts アサーション関数 早期エラー化
discriminated union タグ付き共用体の分岐 状態管理・API応答
Equality narrowing リテラル比較で絞り込み リテラル型
Truthiness narrowing 真偽値判定で絞り込み null/undefined除外

typeof による narrowing

typeof演算子はJavaScript由来ですが、TypeScriptはこの演算子をコード上で見つけるとその分岐内で変数の型を自動的に絞り込みます。プリミティブ型の判定で最も使われる基本テクニックです。

typeofで判定できる値の種類

typeofが返す文字列は7種類あります。TypeScriptはこれらすべてに対してnarrowingを認識します。

// typeofが返す値の一覧
typeof "hello";       // "string"
typeof 42;            // "number"
typeof true;          // "boolean"
typeof undefined;     // "undefined"
typeof Symbol();      // "symbol"
typeof 10n;           // "bigint"
typeof function(){};  // "function"
typeof {};            // "object"
typeof null;          // "object" ← 要注意
typeof [];            // "object" ← 配列もobject扱い

基本: string/number 判定

最もよく使うパターンです。引数のユニオン型を分岐ごとに絞り込みます。

function formatValue(value: string | number): string {
  if (typeof value === "string") {
    return value.toUpperCase(); // valueはstring
  }
  return value.toFixed(2); // valueはnumber
}

console.log(formatValue("hello")); // "HELLO"
console.log(formatValue(3.14159)); // "3.14"

3つ以上のユニオンを絞り込む

3種類以上のプリミティブが混在する場合も同じ要領で書けます。

type Primitive = string | number | boolean;

function describe(value: Primitive): string {
  if (typeof value === "string") return `文字列: ${value}`;
  if (typeof value === "number") return `数値: ${value}`;
  return `真偽値: ${value}`; // ここではbooleanに絞り込まれている
}

typeof ‘function’ で関数を判定

引数が値か関数かで処理を分けたい場合、typeof === "function"が使えます。ReactのuseStateの更新関数(値 or 関数)を扱う際の典型です。

type SetterArg<T> = T | ((prev: T) => T);

function applyUpdate<T>(prev: T, arg: SetterArg<T>): T {
  if (typeof arg === "function") {
    // ここでargは (prev: T) => T に絞り込まれる
    return (arg as (prev: T) => T)(prev);
  }
  return arg; // ここではargはT
}

const next = applyUpdate(10, (p) => p + 1); // 11
const next2 = applyUpdate(10, 99);           // 99

typeof ‘object’ は要注意(nullと配列)

typeof null === "object"であり、配列も"object"です。typeof === "object"だけでは安全ではありません。

// ❌ nullを除外できていない
function getKeys(obj: object | null): string[] {
  if (typeof obj === "object") {
    return Object.keys(obj); // objはobject | null → ランタイムエラーの可能性
  }
  return [];
}

正しくはnullチェックを追加します。

// ✅ nullを除外
function getKeys(obj: object | null): string[] {
  if (typeof obj === "object" && obj !== null) {
    return Object.keys(obj); // ここではobjはobject(非null)
  }
  return [];
}

instanceof による narrowing

instanceofクラスインスタンスの判定に使います。Date / Error / 独自クラスなど、コンストラクタを持つオブジェクトの絞り込みに最適です。

Errorクラスの絞り込み

try/catchで受け取ったerror: unknownを扱う場面は実務で頻出します。

async function fetchUser(id: string) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return await res.json();
  } catch (error) {
    if (error instanceof Error) {
      console.error("error.message:", error.message); // ここでerrorはError
    } else {
      console.error("unknown error:", error);
    }
  }
}

独自エラークラスの階層分岐

カスタムエラーを定義しておくと、エラー種別ごとに処理を分けられます。

class NetworkError extends Error {
  constructor(public status: number, msg: string) {
    super(msg);
    this.name = "NetworkError";
  }
}

class ValidationError extends Error {
  constructor(public field: string, msg: string) {
    super(msg);
    this.name = "ValidationError";
  }
}

function handle(err: unknown): void {
  if (err instanceof NetworkError) {
    console.log("ネットワークエラー:", err.status);
  } else if (err instanceof ValidationError) {
    console.log("入力エラー:", err.field);
  } else if (err instanceof Error) {
    console.log("一般エラー:", err.message);
  } else {
    console.log("未知のエラー:", err);
  }
}

Date型の絞り込み

受け取った値が文字列かDateか分からない場合、instanceof Dateで判別できます。

function toISOString(value: string | Date): string {
  if (value instanceof Date) {
    return value.toISOString(); // valueはDate
  }
  return new Date(value).toISOString(); // valueはstring
}

console.log(toISOString(new Date()));
console.log(toISOString("2026-05-26"));

クラス階層での narrowing

継承関係を持つクラス間でもinstanceofは正しく動作します。サブクラスを判定すると親クラスのチェックは省けます。

class Animal {
  constructor(public name: string) {}
}
class Dog extends Animal {
  bark() { return "Woof"; }
}
class Cat extends Animal {
  meow() { return "Nyan"; }
}

function speak(animal: Animal) {
  if (animal instanceof Dog) {
    return animal.bark(); // animalはDog
  }
  if (animal instanceof Cat) {
    return animal.meow(); // animalはCat
  }
  return `unknown: ${animal.name}`;
}

in 演算子による narrowing

in演算子は「指定したキーがオブジェクトに存在するか」で型を絞り込みます。クラスを持たないプレーンオブジェクトのユニオン分岐に最適です。

基本: プロパティの有無で分岐

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

function makeSound(pet: Cat | Dog) {
  if ("meow" in pet) {
    pet.meow(); // petはCat
  } else {
    pet.bark(); // petはDog
  }
}

3種類以上のユニオンを in で分岐

3種類以上のオブジェクト型でも、各型に固有のキーがあればinで安全に分岐できます。

type Square = { side: number };
type Circle = { radius: number };
type Rect = { width: number; height: number };

function area(shape: Square | Circle | Rect): number {
  if ("side" in shape) return shape.side ** 2;
  if ("radius" in shape) return Math.PI * shape.radius ** 2;
  return shape.width * shape.height; // shapeはRect
}

in でoptional プロパティを扱うときの落とし穴

optionalプロパティに対してinを使うと、TypeScript 4.9以降は絞り込みが緩くなる場合があります。確実に判定するには値の存在もチェックします。

type Config = {
  name: string;
  debug?: boolean;
};

function isDebug(c: Config): boolean {
  // ❌ "debug" in c は debug:undefined でもtrueになり得る
  // ✅ 値が真かどうかまで確認
  return "debug" in c && c.debug === true;
}

user-defined type guard (x is T)

標準のtypeof / instanceof / inでは表現できない複雑な構造の判定には、戻り値がx is T形式の自作判定関数を作ります。これがuser-defined type guardです。

基本: シンプルなis関数

type Fish = { swim: () => void };
type Bird = { fly: () => void };

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

function move(pet: Fish | Bird) {
  if (isFish(pet)) {
    pet.swim(); // petはFishに絞り込まれる
  } else {
    pet.fly();  // petはBird
  }
}

unknownからの型ガード

APIレスポンスのようなunknownから始まる値の検証では、user-defined type guardが必須になります。

type User = {
  id: string;
  name: string;
  age: number;
};

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

const raw: unknown = JSON.parse('{"id":"u1","name":"Tom","age":30}');
if (isUser(raw)) {
  console.log(raw.name); // 型安全にアクセス可能
}

配列の型ガード

配列に対する型ガードではArray.isArrayと要素チェックを組み合わせます。

function isStringArray(value: unknown): value is string[] {
  return Array.isArray(value) && value.every((v) => typeof v === "string");
}

const data: unknown = ["a", "b", "c"];
if (isStringArray(data)) {
  console.log(data.map((s) => s.toUpperCase())); // 型安全
}

ジェネリックな型ガード

任意の要素型の配列を判定する汎用関数です。要素判定関数を引数で受け取ります。

function isArrayOf<T>(
  value: unknown,
  guard: (item: unknown) => item is T
): value is T[] {
  return Array.isArray(value) && value.every(guard);
}

const isNumber = (v: unknown): v is number => typeof v === "number";

const nums: unknown = [1, 2, 3];
if (isArrayOf(nums, isNumber)) {
  console.log(nums.reduce((a, b) => a + b)); // number[]として扱える
}

discriminated union と網羅性チェック

discriminated union(判別可能なユニオン)は、共通の「タグ」プロパティでユニオン型の各分岐を区別する手法です。状態管理・Redux・APIレスポンス処理など、現代TypeScript設計の中心的なパターンです。

基本: tagプロパティで分岐

type Loading = { status: "loading" };
type Success = { status: "success"; data: string };
type Failure = { status: "failure"; error: string };

type State = Loading | Success | Failure;

function render(state: State): string {
  switch (state.status) {
    case "loading":
      return "読み込み中...";
    case "success":
      return `データ: ${state.data}`; // stateはSuccessに絞り込み
    case "failure":
      return `エラー: ${state.error}`; // stateはFailureに絞り込み
  }
}

網羅性チェック(never)

switch文で全ケースを処理し忘れたまま新しい分岐型を追加すると、バグの温床になります。never型を使えば、未処理の分岐があればコンパイルエラーで気づけます。

function assertNever(value: never): never {
  throw new Error(`Unreachable case: ${JSON.stringify(value)}`);
}

function render2(state: State): string {
  switch (state.status) {
    case "loading": return "読み込み中...";
    case "success": return state.data;
    case "failure": return state.error;
    default:
      return assertNever(state); // 全ケース網羅していればstateはnever
  }
}

新しい状態を追加するとコンパイルエラー

例えばtype StateIdleを追加した場合、render2defaultassertNever(state)がエラーになり、未処理の状態に気づけます。

// 状態を追加
type Idle = { status: "idle" };
type State2 = Loading | Success | Failure | Idle;

function render3(state: State2): string {
  switch (state.status) {
    case "loading": return "loading";
    case "success": return state.data;
    case "failure": return state.error;
    // ❌ "idle" を処理していないので↓でエラー
    // default: return assertNever(state); // Type 'Idle' is not assignable to 'never'
    default: return "todo";
  }
}

Reduxスタイルのアクション分岐

reducer関数こそdiscriminated unionの真骨頂です。

type Action =
  | { type: "ADD_TODO"; text: string }
  | { type: "REMOVE_TODO"; id: number }
  | { type: "TOGGLE_TODO"; id: number };

type Todo = { id: number; text: string; done: boolean };

function reducer(state: Todo[], action: Action): Todo[] {
  switch (action.type) {
    case "ADD_TODO":
      return [...state, { id: Date.now(), text: action.text, done: false }];
    case "REMOVE_TODO":
      return state.filter((t) => t.id !== action.id);
    case "TOGGLE_TODO":
      return state.map((t) =>
        t.id === action.id ? { ...t, done: !t.done } : t
      );
  }
}

tagged union vs discriminated union

「tagged union」と「discriminated union」は実質的に同じ概念で、関数型言語界隈でtagged、TypeScript界隈でdiscriminatedと呼ばれます。本記事では一貫してdiscriminated unionと呼びます。

asserts キーワードによるアサーション関数

asserts「これが偽なら例外を投げる」型のアサーション関数を作るためのキーワードです。x is Tがtrue/falseで分岐するのに対し、asserts関数を抜けた時点で型が確定します。

基本: asserts value is T

function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== "string") {
    throw new Error("Not a string");
  }
}

function shout(value: unknown): string {
  assertIsString(value);
  return value.toUpperCase(); // ここではvalueはstring
}

非null アサーション関数

nullable な値を非nullに保証する関数も自前で書けます。

function assertDefined<T>(value: T | null | undefined): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error("value is null/undefined");
  }
}

function useValue(maybe: string | null) {
  assertDefined(maybe);
  console.log(maybe.length); // maybeはstring
}

条件式そのものをassertする

真偽値そのものをアサートする形式も書けます。Node.jsのassertと同じ動作です。

function assert(condition: unknown, msg?: string): asserts condition {
  if (!condition) throw new Error(msg ?? "Assertion failed");
}

function safeDivide(a: number, b: number): number {
  assert(b !== 0, "0除算は禁止");
  return a / b;
}

unknown と JSONパースの安全な扱い

anyは型チェックを完全に無効化しますが、unknownは「型ガードを通さないと何もできない」厳格な型です。APIレスポンスやJSON.parseの戻り値は必ずunknownで受けるのがベストプラクティスです。

any と unknown の違い

const a: any = JSON.parse('{"x": 1}');
console.log(a.x.y.z); // ✅ コンパイル通る(でも実行時にクラッシュの可能性)

const u: unknown = JSON.parse('{"x": 1}');
// console.log(u.x); // ❌ コンパイルエラー: Object is of type 'unknown'

// 型ガードで絞り込むまで何もできない
if (typeof u === "object" && u !== null && "x" in u) {
  console.log((u as { x: unknown }).x);
}

JSONパース結果を安全に扱う

function safeParse<T>(
  raw: string,
  guard: (value: unknown) => value is T
): T | null {
  try {
    const parsed: unknown = JSON.parse(raw);
    return guard(parsed) ? parsed : null;
  } catch {
    return null;
  }
}

type Point = { x: number; y: number };
const isPoint = (v: unknown): v is Point =>
  typeof v === "object" && v !== null &&
  typeof (v as Point).x === "number" &&
  typeof (v as Point).y === "number";

const p = safeParse('{"x":1,"y":2}', isPoint);
if (p) console.log(p.x + p.y);

fetch+型ガードの定石パターン

async function fetchTyped<T>(
  url: string,
  guard: (v: unknown) => v is T
): Promise<T> {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const json: unknown = await res.json();
  if (!guard(json)) throw new Error("Invalid response shape");
  return json;
}

const user = await fetchTyped("/api/me", isUser);
console.log(user.name); // 完全に型安全

その他の narrowing パターン

Equality narrowing(等価比較で絞り込み)

===での比較もnarrowingに使われます。リテラル型の絞り込みで効果を発揮します。

type Direction = "north" | "south" | "east" | "west";

function move(dir: Direction): [number, number] {
  if (dir === "north") return [0, 1];
  if (dir === "south") return [0, -1];
  if (dir === "east") return [1, 0];
  return [-1, 0]; // dirは"west"に絞り込まれている
}

Truthiness narrowing(真偽値で絞り込み)

if (value)のような真偽判定も型を絞り込みます。null/undefinedを除外する典型用途です。

function greet(name: string | null | undefined) {
  if (name) {
    console.log(name.toUpperCase()); // nameはstring(non-empty)
  } else {
    console.log("名無しさん"); // nameはnull | undefined | ""
  }
}

0や空文字の落とし穴

Truthiness narrowing は0 / "" / falseもfalsyと判定するため、numberでは要注意です。

// ❌ 0が「未指定」と誤判定される
function withDefault(n: number | undefined): number {
  if (!n) return 10; // n === 0 のときも10にしてしまう
  return n;
}

// ✅ undefinedだけを除外
function withDefault2(n: number | undefined): number {
  if (n === undefined) return 10;
  return n;
}

Nullish coalescing と narrowing

??nullundefinedのみをデフォルト値で置き換えます。||と違って0""を誤って置換しません。

function port(n: number | undefined): number {
  return n ?? 3000; // n === 0 のときは0を返す
}

console.log(port(0));         // 0
console.log(port(undefined)); // 3000

Array.isArray による narrowing

配列とオブジェクトの分岐にはArray.isArrayが最適です。

function flatten(value: string | string[]): string {
  if (Array.isArray(value)) {
    return value.join(", "); // valueはstring[]
  }
  return value; // valueはstring
}

console.log(flatten(["a", "b", "c"])); // "a, b, c"
console.log(flatten("hello"));         // "hello"

制御フロー解析(Control Flow Analysis)の挙動

TypeScriptは早期return / throw / 例外などを認識し、それ以降のスコープで型を再計算します。

function process(input: string | null) {
  if (input === null) {
    throw new Error("input required");
  }
  // ここではinputはstringに絞り込まれている
  return input.toUpperCase();
}

typeof + keyof の組み合わせ

オブジェクトのキー判定と値型判定を組み合わせると、動的なフィールドアクセスを型安全にできます。

type Settings = {
  theme: "light" | "dark";
  fontSize: number;
  showLineNumber: boolean;
};

function getSetting<K extends keyof Settings>(
  settings: Settings,
  key: K
): Settings[K] {
  return settings[key];
}

const s: Settings = { theme: "dark", fontSize: 14, showLineNumber: true };
const t = getSetting(s, "theme");        // "light" | "dark"
const f = getSetting(s, "fontSize");     // number

Branded Types と型ガード

同じプリミティブ型でも「意味」が違うものを区別したいときに使うのがBranded Types(ブランド型)です。UserIdOrderIdはどちらもstringですが、混同したくないものです。

Branded Type の作り方

type Brand<T, B> = T & { __brand: B };

type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;

function asUserId(s: string): UserId {
  // フォーマット検証等を入れる
  if (!/^u_[a-z0-9]+$/.test(s)) throw new Error("invalid user id");
  return s as UserId;
}

function asOrderId(s: string): OrderId {
  if (!/^o_[a-z0-9]+$/.test(s)) throw new Error("invalid order id");
  return s as OrderId;
}

function getUser(id: UserId) { /* ... */ }

const uid = asUserId("u_001");
getUser(uid);          // ✅ OK
// getUser("u_001");   // ❌ string は UserId に代入できない
// getUser(asOrderId("o_001")); // ❌ OrderIdはUserIdではない

Branded Type + type guard

ブランド型に対するtype guardも作れます。バリデーション関数を併せて提供すると安全です。

function isUserId(value: string): value is UserId {
  return /^u_[a-z0-9]+$/.test(value);
}

function processInput(raw: string) {
  if (isUserId(raw)) {
    // rawはUserIdに絞り込まれる
    getUser(raw);
  }
}

Email型・URL型などの応用

type Email = Brand<string, "Email">;
type Url = Brand<string, "Url">;

const isEmail = (s: string): s is Email => /^[^@]+@[^@]+.[^@]+$/.test(s);
const isUrl = (s: string): s is Url => {
  try { new URL(s); return true; } catch { return false; }
};

function sendMail(to: Email, body: string) { /* ... */ }

const input = "tom@example.com";
if (isEmail(input)) sendMail(input, "hello"); // 型安全に呼び出し

enum と literal union での型ガード

literal union(推奨)

現代TypeScriptではenumよりもliteral unionを推奨します。tree-shake可能で、ランタイムオーバーヘッドがありません。

const ROLE = ["admin", "user", "guest"] as const;
type Role = typeof ROLE[number]; // "admin" | "user" | "guest"

function isRole(value: string): value is Role {
  return (ROLE as readonly string[]).includes(value);
}

const raw = "admin";
if (isRole(raw)) {
  // rawはRoleに絞り込まれる
}

enum を使う場合の型ガード

既存コードにenumがある場合は、値の集合に含まれるかどうかで判定します。

enum Status {
  Draft = "draft",
  Published = "published",
  Archived = "archived",
}

function isStatus(value: string): value is Status {
  return Object.values(Status).includes(value as Status);
}

const input2: string = "draft";
if (isStatus(input2)) {
  console.log(input2); // Statusに絞り込まれる
}

Zod / valibot / io-ts によるランタイム検証連携

本格的なAPIレスポンス検証や複雑なスキーマでは、専用のバリデーションライブラリを使うほうが保守性が高くなります。3大ライブラリの使い方を見ていきます。

Zod(最も人気)

Zodはスキーマ定義から型を自動推論するライブラリで、最も人気があります。

// npm i zod
import { z } from "zod";

const UserSchema = z.object({
  id: z.string(),
  name: z.string().min(1),
  age: z.number().int().nonnegative(),
  email: z.string().email().optional(),
});

type ZUser = z.infer<typeof UserSchema>;
// = { id: string; name: string; age: number; email?: string }

const raw3: unknown = JSON.parse('{"id":"u1","name":"Tom","age":30}');

// parse: 失敗時は例外
const user2 = UserSchema.parse(raw3);

// safeParse: 失敗時はresult.success=false
const result = UserSchema.safeParse(raw3);
if (result.success) {
  console.log(result.data.name);
} else {
  console.error(result.error.issues);
}

Zod + 自作 type guard

Zodスキーマを使ってtype guardを作ることもできます。

function isZUser(value: unknown): value is ZUser {
  return UserSchema.safeParse(value).success;
}

if (isZUser(raw3)) {
  // raw3はZUserに絞り込まれる
}

valibot(軽量・モジュラー)

valibotはZodより小さく、tree-shakeに優れたモジュラー設計が特徴です。

// npm i valibot
import * as v from "valibot";

const ProductSchema = v.object({
  id: v.string(),
  name: v.pipe(v.string(), v.minLength(1)),
  price: v.pipe(v.number(), v.minValue(0)),
});

type Product = v.InferOutput<typeof ProductSchema>;

const rawProduct: unknown = JSON.parse('{"id":"p1","name":"book","price":1000}');
const product = v.parse(ProductSchema, rawProduct);
console.log(product.name);

// safeParse 相当
const safe = v.safeParse(ProductSchema, rawProduct);
if (safe.success) console.log(safe.output.price);

io-ts(関数型風)

io-tsは関数型プログラミング志向で、fp-tsと組み合わせて使うことが多いライブラリです。

// npm i io-ts fp-ts
import * as t from "io-ts";
import { isRight } from "fp-ts/Either";

const OrderCodec = t.type({
  id: t.string,
  amount: t.number,
  paid: t.boolean,
});

type Order = t.TypeOf<typeof OrderCodec>;

const rawOrder: unknown = JSON.parse('{"id":"o1","amount":2000,"paid":true}');
const decoded = OrderCodec.decode(rawOrder);
if (isRight(decoded)) {
  console.log(decoded.right.id); // Order型として扱える
}

3ライブラリの選び方

ライブラリ 特徴 バンドルサイズ 向いている場面
Zod 人気・情報量豊富 中(~57KB) SaaS・チーム開発・迷ったら
valibot 軽量・モジュラー 小(~13KB) バンドルサイズ重視・エッジ
io-ts 関数型志向 fp-ts併用プロジェクト

React Props と APIレスポンスでの実戦活用

React Props の discriminated union

「アイコンありボタン」と「テキストのみボタン」のような排他的Propsを表現するときに最適です。

type ButtonProps =
  | { variant: "icon"; icon: string; label?: string }
  | { variant: "text"; label: string };

function Button(props: ButtonProps) {
  if (props.variant === "icon") {
    return <button>{props.icon}{props.label ?? ""}</button>;
  }
  return <button>{props.label}</button>;
}

// 使用例
<Button variant="icon" icon="🔍" />
<Button variant="text" label="送信" />
// ❌ <Button variant="icon" /> → icon必須エラー

React useReducer + discriminated union

type CounterAction =
  | { type: "increment" }
  | { type: "decrement" }
  | { type: "set"; value: number };

function counterReducer(state: number, action: CounterAction): number {
  switch (action.type) {
    case "increment": return state + 1;
    case "decrement": return state - 1;
    case "set":       return action.value;
  }
}

APIレスポンスの早期return パターン

responseオブジェクトに対する典型的なdiscriminated unionの使い方です。

type ApiResult<T> =
  | { ok: true; data: T }
  | { ok: false; error: string };

async function safeFetch<T>(
  url: string,
  guard: (v: unknown) => v is T
): Promise<ApiResult<T>> {
  try {
    const res = await fetch(url);
    if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
    const json: unknown = await res.json();
    if (!guard(json)) return { ok: false, error: "shape mismatch" };
    return { ok: true, data: json };
  } catch (e) {
    return { ok: false, error: e instanceof Error ? e.message : "unknown" };
  }
}

const result2 = await safeFetch("/api/me", isUser);
if (result2.ok) {
  console.log(result2.data.name); // 型安全
} else {
  console.error(result2.error);
}

アンチパターンとリファクタリング

アンチパターン1: as any による絞り込み

as anyは型チェックを完全に潰すため、絶対に使わないでください。

// ❌ 悪い例
function badParse(value: unknown): User {
  return value as any; // ランタイムでクラッシュの可能性
}

// ✅ 良い例: type guardで検証
function goodParse(value: unknown): User {
  if (!isUser(value)) throw new Error("invalid user");
  return value;
}

アンチパターン2: 二重キャスト(as unknown as T)

「コンパイラを黙らせるだけ」の二重キャストは、ほぼ全てtype guardやスキーマ検証で置き換えるべきです。

// ❌ 悪い例
const raw4 = JSON.parse('{"x":1}');
const user3 = raw4 as unknown as User; // 型はUserになるが実態は別物

// ✅ 良い例
const raw5: unknown = JSON.parse('{"x":1}');
if (isUser(raw5)) {
  const user4 = raw5; // 型安全
}

アンチパターン3: 過剰な non-null アサーション(!)

// ❌ 悪い例
function getFirst(arr: number[] | undefined): number {
  return arr![0]; // arrがundefinedならランタイムエラー
}

// ✅ 良い例
function getFirstSafe(arr: number[] | undefined): number | undefined {
  if (!arr || arr.length === 0) return undefined;
  return arr[0];
}

アンチパターン4: 型ガード関数が偽陽性を返す

user-defined type guardの戻り値x is TはTypeScriptに対する「約束」です。中身がデタラメだとランタイムで不整合が起きます。

// ❌ 危険: 検証していないのに「is T」を返す
function badGuard(v: unknown): v is User {
  return true; // 何でもUserと言ってしまう
}

// ✅ 各フィールドを真面目に確認
function goodGuard(v: unknown): v is User {
  return (
    typeof v === "object" && v !== null &&
    typeof (v as User).id === "string" &&
    typeof (v as User).name === "string" &&
    typeof (v as User).age === "number"
  );
}

Before/After: ネストしたif地獄をdiscriminated unionに

「これloading?それともerror?それともdata?」と複数のbooleanで状態管理しているコードは、必ずdiscriminated unionで置き換えられます。

// ❌ Before: 不正な状態を表現できてしまう
type BadState = {
  isLoading: boolean;
  isError: boolean;
  data: User | null;
  error: string | null;
};
// → isLoading:true かつ data:あり かつ error:あり、のような不正状態が作れる

// ✅ After: discriminated unionで不正状態を構造的に禁止
type GoodState =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: User }
  | { status: "error"; error: string };

Before/After: 文字列比較の連鎖をswitchに

// ❌ Before
function route(path: string) {
  if (path === "/") return renderHome();
  if (path === "/about") return renderAbout();
  if (path === "/contact") return renderContact();
  return renderNotFound();
}

// ✅ After: literal union + switch + 網羅性チェック
type Path = "/" | "/about" | "/contact";
const isPath = (s: string): s is Path =>
  s === "/" || s === "/about" || s === "/contact";

function route2(path: string) {
  if (!isPath(path)) return renderNotFound();
  switch (path) {
    case "/":        return renderHome();
    case "/about":   return renderAbout();
    case "/contact": return renderContact();
    default:         return assertNever(path);
  }
}

declare function renderHome(): void;
declare function renderAbout(): void;
declare function renderContact(): void;
declare function renderNotFound(): void;

まとめ:型ガードを使いこなすための指針

TypeScriptの型ガードは「静的型システムとランタイム値の橋渡し」を担う中核機能です。本記事で扱った40以上のコードサンプルを身につければ、以下のような効果が期待できます。

型ガード習得後に得られるメリット

  • anyを使わずにunknownから始まる値を安全にハンドリングできる
  • discriminated unionで不正な状態を「構造的に表現不可能」にできる
  • 網羅性チェックにより、新ケース追加時の漏れがコンパイル時に発覚する
  • Zod等のスキーマ検証ライブラリを併用したAPIレスポンス処理ができる
  • Branded Typesで意味の異なる同型値を取り違えない設計ができる

使い分けのチートシート

場面 推奨手法
プリミティブのユニオン判定 typeof
クラスインスタンス・Error判定 instanceof
オブジェクトのプロパティ有無 in
複雑な構造の検証 user-defined type guard (is)
前提条件の保証 asserts
状態管理・アクション分岐 discriminated union + switch + never
APIレスポンス検証 Zod / valibot / io-ts
意味の違う同型値の区別 Branded Types

関連記事

型ガードは覚えれば覚えるほど、コードが安全になり、リファクタリングが怖くなくなるテクニックです。今日からはまず「APIレスポンスをunknownで受けて、user-defined type guardかZodで検証してから使う」習慣を身につけてみてください。それだけで現場のバグが目に見えて減ります。

コメント

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