はじめに

TypeScriptでユニオン型を使っていると、「このオブジェクトは本当にUserなのか、それともGuestなのか」「この値はstringかもしれないしnumberかもしれない」といった場面に頻繁に遭遇します。こうした複数の型を持ちうる値を、条件分岐によって安全に扱うための仕組みが「型ガード(Type Guard)」です。

本記事では、TypeScriptにおける型ガードの全手法を網羅的に解説します。組み込みの演算子を使った型ガードから、独自のロジックで型を判定するユーザー定義型ガード、型アサーションとの使い分け、そしてnever型を活用した網羅性チェックまで、実務で必要となる知識を体系的に学べます。

この記事を読み終えると、以下のことができるようになります。

  • typeofinstanceofin演算子による型ガードを適切に使い分けられる
  • ユーザー定義型ガード(is)で独自の型判定ロジックを実装できる
  • 型アサーション(as)の適切な使用場面と危険性を理解できる
  • never型を使った網羅性チェックで、型の追加時に漏れを防げる

実行環境・前提条件

前提知識

動作確認環境

ツール バージョン
Node.js 20.x以上
TypeScript 5.7以上
VS Code 最新版

本記事のサンプルコードは、TypeScript Playgroundで動作確認できます。ローカル環境で実行する場合は、開発環境構築ガイドを参照してください。

期待される結果

本記事のコードを実行すると、以下の動作を確認できます。

  • 型ガードによって適切な型に絞り込まれた状態でプロパティやメソッドにアクセスできる
  • ユーザー定義型ガードで独自の判定ロジックが型システムと連携する
  • 網羅性チェックでユニオン型の全ケースを処理できる

TypeScript型ガードとは

型ガード(Type Guard)とは、実行時の条件分岐によってTypeScriptコンパイラに「この時点でこの変数は特定の型である」と伝える仕組みです。型ガードを使うことで、ユニオン型の変数を特定の型に「絞り込む(Narrowing)」ことができます。

flowchart TD
    A["変数: string | number"] --> B{型ガード}
    B -->|typeof x === 'string'| C["型: string"]
    B -->|typeof x === 'number'| D["型: number"]
    C --> E["stringのメソッドが使える"]
    D --> F["numberの演算ができる"]

型ガードが必要な理由

以下のコードは、型ガードがないとコンパイルエラーになります。

1
2
3
4
function processValue(value: string | number) {
  // エラー: Property 'toUpperCase' does not exist on type 'string | number'.
  // return value.toUpperCase();
}

valuestringまたはnumberのどちらかですが、TypeScriptはどちらか分からないため、string専用のtoUpperCaseメソッドを呼び出すことを許可しません。

型ガードを使うことで、特定の条件下ではvaluestringであることをコンパイラに伝えられます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function processValue(value: string | number): string {
  if (typeof value === "string") {
    // この時点でvalueはstring型に絞り込まれる
    return value.toUpperCase();
  }
  // この時点でvalueはnumber型に絞り込まれる
  return value.toFixed(2);
}

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

typeof演算子による型ガード

typeof演算子は、JavaScriptのプリミティブ型を判定する最も基本的な型ガードです。

typeofが返す値

typeof演算子は以下の文字列を返します。

値の型 typeofの結果
string "string"
number "number"
bigint "bigint"
boolean "boolean"
symbol "symbol"
undefined "undefined"
null "object"
関数 "function"
その他のオブジェクト "object"

typeof型ガードの基本

typeofを使った型ガードの基本パターンを見てみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function formatValue(value: string | number | boolean): string {
  if (typeof value === "string") {
    // value: string
    return `文字列: "${value}"`;
  }
  if (typeof value === "number") {
    // value: number
    return `数値: ${value.toFixed(2)}`;
  }
  // value: boolean
  return `真偽値: ${value ? "真" : "偽"}`;
}

console.log(formatValue("TypeScript")); // 文字列: "TypeScript"
console.log(formatValue(42.5));         // 数値: 42.50
console.log(formatValue(true));         // 真偽値: 真

typeofの注意点

typeof null"object"を返すという、JavaScriptの歴史的な仕様があります。この挙動には注意が必要です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
function processData(data: string[] | null): void {
  // 不完全なガード - nullも"object"と判定される
  if (typeof data === "object") {
    // data: string[] | null - nullが除外されていない!
    // data.forEach(...) // ランタイムエラーの可能性
  }
}

// 正しいガード
function processDataSafe(data: string[] | null): void {
  if (data !== null && typeof data === "object") {
    // data: string[] - nullが正しく除外される
    data.forEach((item) => console.log(item));
  }
}

nullを含むユニオン型を扱う場合は、明示的にnullチェックを組み合わせましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
function printAll(strs: string | string[] | null): void {
  if (strs !== null) {
    if (typeof strs === "object") {
      // strs: string[] - nullは既に除外済み
      for (const s of strs) {
        console.log(s);
      }
    } else if (typeof strs === "string") {
      // strs: string
      console.log(strs);
    }
  }
}

printAll("hello");           // hello
printAll(["a", "b", "c"]);   // a, b, c
printAll(null);              // (何も出力されない)

instanceof演算子による型ガード

instanceof演算子は、オブジェクトが特定のクラス(コンストラクタ関数)のインスタンスかどうかを判定します。クラスベースのオブジェクト指向プログラミングで頻繁に使用される型ガードです。

instanceofの基本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Dog {
  bark(): string {
    return "ワン!";
  }
}

class Cat {
  meow(): string {
    return "ニャー!";
  }
}

function makeSound(animal: Dog | Cat): string {
  if (animal instanceof Dog) {
    // animal: Dog
    return animal.bark();
  }
  // animal: Cat
  return animal.meow();
}

const dog = new Dog();
const cat = new Cat();

console.log(makeSound(dog)); // ワン!
console.log(makeSound(cat)); // ニャー!

組み込みオブジェクトでのinstanceof

DateErrorなどの組み込みオブジェクトにもinstanceofを使用できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function formatDateOrString(value: Date | string): string {
  if (value instanceof Date) {
    // value: Date
    return value.toISOString();
  }
  // value: string
  return value;
}

console.log(formatDateOrString(new Date("2026-01-03"))); // 2026-01-03T00:00:00.000Z
console.log(formatDateOrString("2026-01-03"));           // 2026-01-03

エラーハンドリングでのinstanceof

instanceofはエラーハンドリングで特に有用です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class ValidationError extends Error {
  constructor(
    message: string,
    public field: string
  ) {
    super(message);
    this.name = "ValidationError";
  }
}

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

function handleError(error: Error): string {
  if (error instanceof ValidationError) {
    // error: ValidationError
    return `バリデーションエラー - フィールド: ${error.field}, メッセージ: ${error.message}`;
  }
  if (error instanceof NetworkError) {
    // error: NetworkError
    return `ネットワークエラー - ステータス: ${error.statusCode}, メッセージ: ${error.message}`;
  }
  // error: Error
  return `予期しないエラー: ${error.message}`;
}

console.log(handleError(new ValidationError("必須項目です", "email")));
// バリデーションエラー - フィールド: email, メッセージ: 必須項目です

console.log(handleError(new NetworkError("サーバーエラー", 500)));
// ネットワークエラー - ステータス: 500, メッセージ: サーバーエラー

instanceofの制限事項

instanceofはクラスに対してのみ使用可能で、インターフェースや型エイリアスには使用できません。インターフェースは実行時に存在しないためです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
interface User {
  name: string;
  email: string;
}

interface Admin {
  name: string;
  role: string;
}

// エラー: 'User' only refers to a type, but is being used as a value here.
// function isUser(obj: User | Admin): boolean {
//   return obj instanceof User;
// }

インターフェースを判別するには、in演算子やユーザー定義型ガードを使用します。

in演算子による型ガード

in演算子は、オブジェクトに特定のプロパティが存在するかどうかを判定します。インターフェースや型エイリアスで定義されたオブジェクト型を判別する際に有効です。

in演算子の基本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Fish = {
  swim: () => void;
};

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

function move(animal: Fish | Bird): void {
  if ("swim" in animal) {
    // animal: Fish
    animal.swim();
  } else {
    // animal: Bird
    animal.fly();
  }
}

const fish: Fish = { swim: () => console.log("泳いでいます") };
const bird: Bird = { fly: () => console.log("飛んでいます") };

move(fish); // 泳いでいます
move(bird); // 飛んでいます

複数のプロパティによる判別

複数のプロパティを組み合わせて、より精密な型判別を行うこともできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
interface Car {
  brand: string;
  drive: () => void;
}

interface Bicycle {
  brand: string;
  pedal: () => void;
}

interface Boat {
  brand: string;
  sail: () => void;
}

type Vehicle = Car | Bicycle | Boat;

function operate(vehicle: Vehicle): void {
  if ("drive" in vehicle) {
    // vehicle: Car
    console.log(`${vehicle.brand}を運転します`);
    vehicle.drive();
  } else if ("pedal" in vehicle) {
    // vehicle: Bicycle
    console.log(`${vehicle.brand}を漕ぎます`);
    vehicle.pedal();
  } else {
    // vehicle: Boat
    console.log(`${vehicle.brand}で航海します`);
    vehicle.sail();
  }
}

オプショナルプロパティとin演算子

オプショナルプロパティ(?付きプロパティ)を持つ型の場合、in演算子による絞り込みには注意が必要です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
interface Human {
  swim?: () => void;
  walk: () => void;
}

type Creature = Fish | Human;

function moveCreature(creature: Creature): void {
  if ("swim" in creature) {
    // creature: Fish | Human
    // Humanもswimプロパティを持ちうるため、完全に絞り込めない
    creature.swim?.(); // オプショナルチェーンが必要
  }
  if ("walk" in creature) {
    // creature: Human
    creature.walk();
  }
}

オプショナルプロパティを含む型を正確に判別するには、ディスクリミネーテッドユニオン(判別可能なユニオン)パターンの使用を検討してください。

ユーザー定義型ガード(is)

TypeScriptでは、isキーワードを使って独自の型ガード関数を定義できます。これを「ユーザー定義型ガード」または「型述語(Type Predicate)」と呼びます。

ユーザー定義型ガードの基本構文

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

戻り値の型としてpet is Fishと記述することで、この関数がtrueを返した場合に引数petFish型であることをTypeScriptに伝えます。

ユーザー定義型ガードの活用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
interface Fish {
  swim: () => void;
  name: string;
}

interface Bird {
  fly: () => void;
  name: string;
}

// ユーザー定義型ガード
function isFish(pet: Fish | Bird): pet is Fish {
  return "swim" in pet;
}

function isBird(pet: Fish | Bird): pet is Bird {
  return "fly" in pet;
}

function getPetAction(pet: Fish | Bird): string {
  if (isFish(pet)) {
    // pet: Fish
    return `${pet.name}は泳ぎます`;
  }
  // pet: Bird
  return `${pet.name}は飛びます`;
}

const nemo: Fish = { name: "ニモ", swim: () => {} };
const tweety: Bird = { name: "トゥイーティー", fly: () => {} };

console.log(getPetAction(nemo));   // ニモは泳ぎます
console.log(getPetAction(tweety)); // トゥイーティーは飛びます

配列のフィルタリングでの活用

ユーザー定義型ガードは、配列のfilterメソッドと組み合わせると特に強力です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
interface SuccessResponse {
  status: "success";
  data: string;
}

interface ErrorResponse {
  status: "error";
  message: string;
}

type ApiResponse = SuccessResponse | ErrorResponse;

function isSuccess(response: ApiResponse): response is SuccessResponse {
  return response.status === "success";
}

function isError(response: ApiResponse): response is ErrorResponse {
  return response.status === "error";
}

const responses: ApiResponse[] = [
  { status: "success", data: "ユーザーデータ" },
  { status: "error", message: "認証エラー" },
  { status: "success", data: "商品データ" },
  { status: "error", message: "ネットワークエラー" },
];

// 型ガードを使わない場合 - 型が(SuccessResponse | ErrorResponse)[]のまま
const filtered = responses.filter((r) => r.status === "success");

// 型ガードを使う場合 - 型がSuccessResponse[]になる
const successResponses = responses.filter(isSuccess);
console.log(successResponses.map((r) => r.data)); // ["ユーザーデータ", "商品データ"]

const errorResponses = responses.filter(isError);
console.log(errorResponses.map((r) => r.message)); // ["認証エラー", "ネットワークエラー"]

null/undefinedのフィルタリング

nullundefinedを配列から除外する型ガードも実用的です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function isNotNullOrUndefined<T>(value: T | null | undefined): value is T {
  return value !== null && value !== undefined;
}

const mixedArray: (string | null | undefined)[] = [
  "apple",
  null,
  "banana",
  undefined,
  "cherry",
];

// 型ガードなし - (string | null | undefined)[]のまま
const filteredBasic = mixedArray.filter((x) => x != null);

// 型ガードあり - string[]に絞り込まれる
const fruits = mixedArray.filter(isNotNullOrUndefined);
console.log(fruits); // ["apple", "banana", "cherry"]
// fruitsはstring[]型なので、stringのメソッドが使える
console.log(fruits.map((f) => f.toUpperCase())); // ["APPLE", "BANANA", "CHERRY"]

アサーション関数

TypeScript 3.7以降では、アサーション関数(Assertion Functions)も利用できます。アサーション関数は、条件を満たさない場合に例外をスローし、満たす場合に型を絞り込みます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== "string") {
    throw new Error(`Expected string, got ${typeof value}`);
  }
}

function processInput(input: unknown): string {
  assertIsString(input);
  // この時点でinputはstring型に絞り込まれる
  return input.toUpperCase();
}

console.log(processInput("hello")); // HELLO
// processInput(123); // Error: Expected string, got number

型アサーション(as)

型アサーション(Type Assertion)は、開発者がTypeScriptに「この値は特定の型である」と明示的に伝える構文です。型ガードとは異なり、実行時のチェックを伴いません。

型アサーションの構文

1
2
3
4
5
// as構文(推奨)
const value1 = someValue as string;

// アングルブラケット構文(JSXと競合するため非推奨)
const value2 = <string>someValue;

型アサーションの使用場面

型アサーションは、開発者がTypeScriptよりも正確な型情報を持っている場合に使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// DOM操作での型アサーション
const inputElement = document.getElementById("username") as HTMLInputElement;
inputElement.value = "TypeScript";

// APIレスポンスの型アサーション
interface User {
  id: number;
  name: string;
  email: string;
}

async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const data = await response.json();
  return data as User;
}

型アサーションの危険性

型アサーションは型チェックをバイパスするため、誤用すると実行時エラーの原因となります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
interface User {
  id: number;
  name: string;
}

// 危険な型アサーション
const fakeUser = {} as User;
console.log(fakeUser.name.toUpperCase()); // 実行時エラー!

// 安全なアプローチ - 型ガードを使用
function isUser(obj: unknown): obj is User {
  return (
    typeof obj === "object" &&
    obj !== null &&
    "id" in obj &&
    "name" in obj &&
    typeof (obj as User).id === "number" &&
    typeof (obj as User).name === "string"
  );
}

function processUserData(data: unknown): void {
  if (isUser(data)) {
    // data: User - 安全にアクセス可能
    console.log(data.name.toUpperCase());
  } else {
    console.log("無効なユーザーデータ");
  }
}

constアサーション

as constは特殊な型アサーションで、リテラル型への絞り込みとreadonlyの付与を行います。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 通常の配列 - string[]型
const colors = ["red", "green", "blue"];

// constアサーション - readonly ["red", "green", "blue"]型
const colorsConst = ["red", "green", "blue"] as const;

// オブジェクトでのconstアサーション
const config = {
  endpoint: "https://api.example.com",
  timeout: 5000,
} as const;

// config.endpoint は "https://api.example.com" リテラル型
// config.timeout は 5000 リテラル型
// すべてのプロパティがreadonlyになる

型アサーションと型ガードの使い分け

シナリオ 推奨アプローチ
外部データ(API、ユーザー入力)の処理 型ガード
DOM要素の取得 型アサーション(要素の存在が確実な場合)
コンパイラの推論が不十分な場合 型アサーション(最小限に)
ユニオン型の絞り込み 型ガード

基本方針として、可能な限り型ガードを使用し、型アサーションは「本当に必要な場合のみ」に限定することをおすすめします。

never型を使った網羅性チェック

never型は「決して発生しない値」を表す型です。この特性を活用することで、switch文やif-else文でユニオン型の全ケースを処理していることをコンパイル時に保証できます。

never型の基本

never型には以下の特徴があります。

  • すべての型のサブタイプである(どの型にも代入可能)
  • never型以外の型をnever型に代入することはできない

この「他の型を代入できない」という特性を利用して、網羅性チェックを実装します。

flowchart TD
    A["Shape: Circle | Square"] --> B{kind}
    B -->|"circle"| C["Circle型を処理"]
    B -->|"square"| D["Square型を処理"]
    C --> E["残りの型: never"]
    D --> E
    E --> F["全ケース処理完了"]

網羅性チェックの実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
interface Circle {
  kind: "circle";
  radius: number;
}

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

type Shape = Circle | Square;

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    default:
      // すべてのケースを処理済みなので、shapeはnever型
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

console.log(getArea({ kind: "circle", radius: 5 }));  // 78.539...
console.log(getArea({ kind: "square", sideLength: 4 })); // 16

新しい型の追加時にエラーを検出

網羅性チェックの真価は、ユニオン型に新しい型を追加したときに発揮されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
interface Circle {
  kind: "circle";
  radius: number;
}

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

// 新しく三角形を追加
interface Triangle {
  kind: "triangle";
  base: number;
  height: number;
}

type Shape = Circle | Square | Triangle;

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    // case "triangle" を忘れている!
    default:
      // エラー: Type 'Triangle' is not assignable to type 'never'.
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

Triangleのケースを追加し忘れると、コンパイルエラーになります。これにより、型の追加時に処理の漏れを防ぐことができます。

assertNever関数パターン

網羅性チェックを再利用可能な関数として定義することもできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function assertNever(value: never, message?: string): never {
  throw new Error(message ?? `Unexpected value: ${value}`);
}

type Status = "pending" | "approved" | "rejected";

function getStatusMessage(status: Status): string {
  switch (status) {
    case "pending":
      return "審査中です";
    case "approved":
      return "承認されました";
    case "rejected":
      return "却下されました";
    default:
      return assertNever(status, `未知のステータス: ${status}`);
  }
}

このassertNever関数は、以下の役割を果たします。

  1. コンパイル時に網羅性をチェック
  2. 万が一実行時に到達した場合は明示的なエラーをスロー

実践的なユースケース:状態管理

Reduxなどの状態管理ライブラリでアクションを処理する際に、網羅性チェックは特に有効です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
interface AddTodoAction {
  type: "ADD_TODO";
  payload: { id: string; text: string };
}

interface RemoveTodoAction {
  type: "REMOVE_TODO";
  payload: { id: string };
}

interface ToggleTodoAction {
  type: "TOGGLE_TODO";
  payload: { id: string };
}

type TodoAction = AddTodoAction | RemoveTodoAction | ToggleTodoAction;

interface TodoState {
  todos: { id: string; text: string; completed: boolean }[];
}

function todoReducer(state: TodoState, action: TodoAction): TodoState {
  switch (action.type) {
    case "ADD_TODO":
      return {
        todos: [
          ...state.todos,
          { id: action.payload.id, text: action.payload.text, completed: false },
        ],
      };
    case "REMOVE_TODO":
      return {
        todos: state.todos.filter((todo) => todo.id !== action.payload.id),
      };
    case "TOGGLE_TODO":
      return {
        todos: state.todos.map((todo) =>
          todo.id === action.payload.id
            ? { ...todo, completed: !todo.completed }
            : todo
        ),
      };
    default:
      const _exhaustiveCheck: never = action;
      return _exhaustiveCheck;
  }
}

新しいアクション型を追加したときに、reducerの更新を忘れるとコンパイルエラーになります。

型ガードのベストプラクティス

型ガードの選択指針

flowchart TD
    A["型を絞り込みたい"] --> B{判別対象}
    B -->|プリミティブ型| C["typeof"]
    B -->|クラスインスタンス| D["instanceof"]
    B -->|オブジェクトのプロパティ| E["in演算子"]
    B -->|独自の判定ロジック| F["ユーザー定義型ガード is"]
    B -->|判別用プロパティあり| G["Discriminated Unions"]
    G --> H["switch文 + never"]

複合的な型ガードの実装

複雑な型を判別する場合は、複数の型ガードを組み合わせます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
interface ApiSuccessResponse<T> {
  success: true;
  data: T;
  timestamp: number;
}

interface ApiErrorResponse {
  success: false;
  error: {
    code: string;
    message: string;
  };
  timestamp: number;
}

type ApiResponse<T> = ApiSuccessResponse<T> | ApiErrorResponse;

// 型ガード関数
function isApiSuccess<T>(
  response: ApiResponse<T>
): response is ApiSuccessResponse<T> {
  return response.success === true;
}

function isApiError<T>(
  response: ApiResponse<T>
): response is ApiErrorResponse {
  return response.success === false;
}

// unknownからApiResponseへの型ガード
function isApiResponse(value: unknown): value is ApiResponse<unknown> {
  if (typeof value !== "object" || value === null) {
    return false;
  }
  const obj = value as Record<string, unknown>;
  if (typeof obj.success !== "boolean" || typeof obj.timestamp !== "number") {
    return false;
  }
  if (obj.success) {
    return "data" in obj;
  }
  return (
    typeof obj.error === "object" &&
    obj.error !== null &&
    "code" in obj.error &&
    "message" in obj.error
  );
}

// 使用例
async function fetchData<T>(url: string): Promise<T | null> {
  const res = await fetch(url);
  const json: unknown = await res.json();

  if (!isApiResponse(json)) {
    console.error("無効なレスポンス形式");
    return null;
  }

  if (isApiSuccess<T>(json as ApiResponse<T>)) {
    return json.data;
  }

  console.error(`APIエラー: ${json.error.code} - ${json.error.message}`);
  return null;
}

型ガードのテスト

型ガード関数は、ユニットテストで検証することをおすすめします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 型ガード関数
function isUser(obj: unknown): obj is User {
  return (
    typeof obj === "object" &&
    obj !== null &&
    "id" in obj &&
    "name" in obj &&
    typeof (obj as User).id === "number" &&
    typeof (obj as User).name === "string"
  );
}

// テストコード(Jest使用)
describe("isUser", () => {
  test("有効なUserオブジェクトでtrueを返す", () => {
    const user = { id: 1, name: "太郎" };
    expect(isUser(user)).toBe(true);
  });

  test("nullでfalseを返す", () => {
    expect(isUser(null)).toBe(false);
  });

  test("idがないオブジェクトでfalseを返す", () => {
    expect(isUser({ name: "太郎" })).toBe(false);
  });

  test("idが数値でないオブジェクトでfalseを返す", () => {
    expect(isUser({ id: "1", name: "太郎" })).toBe(false);
  });
});

まとめ

本記事では、TypeScriptにおける型ガードの全手法を解説しました。

  • typeof演算子: プリミティブ型の判別に使用。null"object"を返す点に注意
  • instanceof演算子: クラスインスタンスの判別に使用。インターフェースには使用不可
  • in演算子: オブジェクトのプロパティ存在確認による判別。オプショナルプロパティに注意
  • ユーザー定義型ガード(is): 独自の判定ロジックを型システムと連携。配列のフィルタリングで特に有効
  • 型アサーション(as): 開発者が型情報を明示。必要最小限の使用を推奨
  • never型と網羅性チェック: ユニオン型の全ケース処理を保証。型追加時の漏れを防止

型ガードを適切に活用することで、実行時の条件分岐とTypeScriptの型システムを連携させ、安全で保守性の高いコードを書くことができます。特に、外部データの処理や複雑なユニオン型を扱う場面では、型ガードが強力な武器となります。

参考リンク