T-CREATOR

TypeScript 型縮小(narrowing)パターン早見表:`in`/`instanceof`/`is`/`asserts`完全対応

TypeScript 型縮小(narrowing)パターン早見表:`in`/`instanceof`/`is`/`asserts`完全対応

TypeScript で開発していると、「この変数の型がもう少し具体的にわかれば...」と感じる場面がよくありますね。そんな時に威力を発揮するのが型縮小(Type Narrowing)です。今回は実践的に使える 4 つの主要パターンを、具体的なコード例とともに詳しく解説いたします。

型縮小パターン早見表

まずは 4 つの主要パターンを一覧で確認しましょう。

パターン構文例主な用途特徴
in オペレータ'property' in objオブジェクトのプロパティ存在チェックシンプルで直感的。Union 型の判定に最適
instanceof オペレータobj instanceof Classクラスインスタンスの判定継承関係に対応。オブジェクト指向設計で有効
is 型ガード(x): x is Typeカスタム条件による判定柔軟性が高い。複雑なロジックを再利用可能
asserts アサーションasserts x is Typeバリデーション+型保証例外処理と型安全性を統合

使い分けの基本指針

mermaidflowchart LR
  start["型縮小が必要"] --> simple{"シンプルな<br/>プロパティ判定?"};
  simple --|Yes|--> in_choice["in オペレータ"];
  simple --|No|--> class_check{"クラスの<br/>インスタンス?"};
  class_check --|Yes|--> instanceof_choice["instanceof"];
  class_check --|No|--> complex{"複雑な条件や<br/>再利用性重視?"};
  complex --|Yes|--> is_choice["is 型ガード"];
  complex --|No|--> validation{"バリデーション<br/>必須?"};
  validation --|Yes|--> asserts_choice["asserts"];
  validation --|No|--> other["typeof など<br/>その他手法"];

各パターンのメリット・デメリット

パターン✅ メリット⚠️ 注意点
in・コードが簡潔・TypeScript が自動推論・実行コストが低い・optional プロパティでは効果薄・文字列リテラル必須
instanceof・継承チェーンに対応・オブジェクトの出自が明確・実行コストが低い・プリミティブ型では使用不可・クラス設計への依存
is・柔軟性が非常に高い・ロジックの再利用可能・複雑な条件に対応・実装コストが高め・条件の妥当性は自己責任
asserts・型安全性とランタイムチェックの統合・バリデーション処理が明確・例外処理が必要・パフォーマンス考慮要

型縮小とは何か

型縮小とは、Union 型や any 型など広い型から、より具体的で限定された型へと絞り込む仕組みのことです。TypeScript が実行時の条件分岐を解析して、その分岐内では特定の型であることを保証してくれる機能ですね。

以下の図で、型縮小の基本的な概念を確認してみましょう。

mermaidflowchart TD
  union["Union型<br/>string | number | null"] --> check{"条件チェック"}
  check -->|"typeof value === 'string'"| str["string型に縮小"]
  check -->|"typeof value === 'number'"| num["number型に縮小"]
  check -->|"value === null"| nil["null型に縮小"]

この図が示すように、条件チェックによって広い型から具体的な型へと段階的に絞り込まれていきます。

型縮小を活用することで、以下のようなメリットが得られます:

  • 型安全性の向上:実行時エラーの予防
  • 開発体験の改善:適切な補完機能の提供
  • コードの可読性向上:意図が明確になる

各パターンの詳細解説

TypeScript で使える型縮小パターンは数多くありますが、実務で頻繁に活用される 4 つの手法を体系的に見ていきましょう。

プロパティ存在チェック:in オペレータ

inオペレータは、オブジェクトに特定のプロパティが存在するかどうかをチェックして型を縮小する手法です。Union 型のオブジェクトを扱う際に特に有効ですね。

基本的な使い方

inオペレータの基本的な構文を確認してみましょう。

typescript// 基本的な型定義
type Cat = {
  name: string;
  meow: () => void;
};

type Dog = {
  name: string;
  bark: () => void;
};

type Animal = Cat | Dog;

プロパティの存在チェックによる型縮小の実装は以下のようになります:

typescriptfunction makeSound(animal: Animal) {
  if ('meow' in animal) {
    // この分岐内では animal は Cat型として扱われる
    animal.meow(); // TypeScriptが meow メソッドの存在を保証
    console.log(`${animal.name} が鳴きました`);
  } else {
    // else分岐では自動的に Dog型として推論される
    animal.bark(); // bark メソッドが安全に呼び出せる
    console.log(`${animal.name} が吠えました`);
  }
}

オブジェクト型での実装例

より実践的な例として、API レスポンスの処理を見てみましょう。

typescript// APIレスポンスの型定義
type SuccessResponse = {
  status: 'success';
  data: {
    id: number;
    name: string;
  };
};

type ErrorResponse = {
  status: 'error';
  message: string;
  code: number;
};

type ApiResponse = SuccessResponse | ErrorResponse;

レスポンス処理の実装では、プロパティの存在で型を判定できます:

typescriptfunction handleApiResponse(response: ApiResponse) {
  if ('data' in response) {
    // SuccessResponse型として処理
    console.log(
      `成功: ユーザー${response.data.name}を取得`
    );
    return response.data;
  } else {
    // ErrorResponse型として処理
    console.error(
      `エラー ${response.code}: ${response.message}`
    );
    throw new Error(response.message);
  }
}

ネストしたオブジェクトでも同様に活用できます:

typescripttype UserProfile = {
  basic: {
    name: string;
    email: string;
  };
  premium?: {
    subscriptionId: string;
    features: string[];
  };
};

function displayUserInfo(profile: UserProfile) {
  console.log(`ユーザー: ${profile.basic.name}`);

  if ('premium' in profile && profile.premium) {
    // プレミアム機能の表示
    console.log(
      `プレミアム機能: ${profile.premium.features.join(
        ', '
      )}`
    );
  }
}

注意点と制限事項

inオペレータを使用する際の重要な注意点をご紹介します。

プロパティ名の文字列リテラル プロパティ名は文字列リテラルで指定する必要があります:

typescript// ❌ 変数を使用した場合は型縮小されない
const property = 'meow';
if (property in animal) {
  // animal の型は Animal のまま(縮小されない)
}

// ✅ 文字列リテラルを直接使用
if ('meow' in animal) {
  // 正常に Cat型に縮小される
}

optional プロパティでの挙動 optional プロパティ(?付き)では期待通りに動作しない場合があります:

typescripttype OptionalProp = {
  name: string;
  age?: number; // optional プロパティ
};

function checkOptional(obj: OptionalProp) {
  if ('age' in obj) {
    // obj.age は number | undefined のまま
    // undefined の可能性が残る
    console.log(obj.age.toString()); // ❌ エラーの可能性
  }
}

インスタンス判定:instanceof オペレータ

instanceofオペレータは、オブジェクトが特定のクラスのインスタンスかどうかを判定して型縮小を行う手法です。クラスベースの設計で威力を発揮しますね。

クラスインスタンスでの型縮小

基本的なクラス定義から見てみましょう:

typescriptclass User {
  constructor(public name: string) {}

  greet() {
    return `こんにちは、${this.name}です`;
  }
}

class Admin {
  constructor(
    public name: string,
    public permissions: string[]
  ) {}

  manageUsers() {
    return `${this.name} がユーザーを管理中`;
  }
}

instanceofを使った型縮小の実装:

typescripttype UserType = User | Admin;

function handleUser(user: UserType) {
  if (user instanceof Admin) {
    // この分岐内では user は Admin型
    console.log(user.manageUsers());
    console.log(`権限: ${user.permissions.join(', ')}`);
  } else {
    // else分岐では User型として推論
    console.log(user.greet());
  }
}

// 使用例
const regularUser = new User('田中');
const adminUser = new Admin('佐藤', [
  'read',
  'write',
  'delete',
]);

handleUser(regularUser); // "こんにちは、田中です"
handleUser(adminUser); // "佐藤 がユーザーを管理中"

継承関係での活用

継承関係がある場合の型縮小パターンを確認しましょう:

typescriptclass Animal {
  constructor(public name: string) {}

  makeSound() {
    return `${this.name} が音を出している`;
  }
}

class Dog extends Animal {
  breed: string;

  constructor(name: string, breed: string) {
    super(name);
    this.breed = breed;
  }

  bark() {
    return `${this.name}${this.breed})がワンワン`;
  }
}

class Cat extends Animal {
  constructor(name: string, public isIndoor: boolean) {
    super(name);
  }

  meow() {
    const location = this.isIndoor ? '室内' : '外';
    return `${this.name}${location}でニャーニャー`;
  }
}

継承チェーンでの型縮小処理:

typescriptfunction animalAction(animal: Animal) {
  if (animal instanceof Dog) {
    // Dog型に縮小(Animalのメソッドも使用可能)
    console.log(animal.bark());
    console.log(`犬種: ${animal.breed}`);
  } else if (animal instanceof Cat) {
    // Cat型に縮小
    console.log(animal.meow());
    console.log(
      `飼育環境: ${animal.isIndoor ? '室内' : '屋外'}`
    );
  } else {
    // 基底クラス Animal として処理
    console.log(animal.makeSound());
  }
}

プリミティブ型での制限

instanceofオペレータはオブジェクト型でのみ有効で、プリミティブ型では期待通りに動作しません:

typescript// ❌ プリミティブ型では正常に動作しない
function checkPrimitive(value: string | number) {
  if (value instanceof String) {
    // これは期待通りに動作しない
    // 通常の文字列リテラルは String クラスのインスタンスではない
  }
}

// ✅ プリミティブ型には typeof を使用
function checkPrimitiveCorrect(value: string | number) {
  if (typeof value === 'string') {
    // 正常に string型に縮小される
    console.log(value.toUpperCase());
  } else {
    // number型として処理
    console.log(value.toFixed(2));
  }
}

組み込みオブジェクトとの使い分けも重要です:

typescript// Date オブジェクトの判定(✅ 正常動作)
function processValue(value: string | Date) {
  if (value instanceof Date) {
    // Date型に縮小
    console.log(value.toISOString());
  } else {
    // string型として処理
    console.log(value.length);
  }
}

// Array の判定も可能(✅ 正常動作)
function handleData(data: string | string[]) {
  if (data instanceof Array) {
    // string[]型に縮小
    console.log(`配列の長さ: ${data.length}`);
    data.forEach((item) => console.log(item));
  } else {
    // string型として処理
    console.log(`文字列: ${data}`);
  }
}

カスタム型ガード:is キーワード

isキーワードを使用したカスタム型ガードは、独自の条件ロジックで型縮小を実現する強力な手法です。複雑な判定条件や再利用可能な型チェック関数を作成できます。

ユーザー定義型ガードの作成

まず、基本的な型ガード関数の構文を確認しましょう:

typescript// 基本的な型定義
interface User {
  id: number;
  name: string;
  email: string;
}

interface Admin {
  id: number;
  name: string;
  permissions: string[];
}

type UserOrAdmin = User | Admin;

isキーワードを使った型ガード関数の実装:

typescript// Admin型かどうかを判定する型ガード関数
function isAdmin(user: UserOrAdmin): user is Admin {
  // permissions プロパティの存在で判定
  return 'permissions' in user;
}

// User型かどうかを判定する型ガード関数
function isUser(user: UserOrAdmin): user is User {
  // email プロパティの存在で判定
  return 'email' in user;
}

型ガード関数を使用した処理の実装:

typescriptfunction processUserData(user: UserOrAdmin) {
  if (isAdmin(user)) {
    // この分岐内では user は Admin型として扱われる
    console.log(`管理者: ${user.name}`);
    console.log(`権限: ${user.permissions.join(', ')}`);
    user.permissions.forEach((permission) => {
      console.log(`- ${permission}`);
    });
  } else if (isUser(user)) {
    // この分岐内では user は User型として扱われる
    console.log(`ユーザー: ${user.name}`);
    console.log(`メール: ${user.email}`);
  }
}

複雑な条件での型縮小

より複雑な判定ロジックを含む型ガードの例を見てみましょう:

typescript// APIレスポンス型の定義
interface ApiSuccessResponse {
  success: true;
  data: {
    id: number;
    timestamp: string;
  };
  metadata?: {
    version: string;
    cached: boolean;
  };
}

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

type ApiResponse = ApiSuccessResponse | ApiErrorResponse;

複雑な条件を含む型ガード関数の実装:

typescript// 成功レスポンスかどうかを詳細に判定
function isSuccessResponse(
  response: ApiResponse
): response is ApiSuccessResponse {
  return (
    response.success === true &&
    'data' in response &&
    typeof response.data === 'object' &&
    response.data !== null &&
    'id' in response.data &&
    'timestamp' in response.data
  );
}

// エラーレスポンスかどうかを判定
function isErrorResponse(
  response: ApiResponse
): response is ApiErrorResponse {
  return (
    response.success === false &&
    'error' in response &&
    typeof response.error === 'object' &&
    'code' in response.error &&
    'message' in response.error
  );
}

型ガードを活用したエラーハンドリング:

typescriptasync function handleApiResponse(response: ApiResponse) {
  if (isSuccessResponse(response)) {
    // ApiSuccessResponse型として安全に処理
    console.log(`データID: ${response.data.id}`);
    console.log(`取得時刻: ${response.data.timestamp}`);

    if (response.metadata) {
      console.log(
        `バージョン: ${response.metadata.version}`
      );
      console.log(
        `キャッシュ: ${
          response.metadata.cached ? 'あり' : 'なし'
        }`
      );
    }

    return response.data;
  } else if (isErrorResponse(response)) {
    // ApiErrorResponse型として処理
    console.error(
      `エラー ${response.error.code}: ${response.error.message}`
    );

    if (response.retryAfter) {
      console.log(
        `${response.retryAfter}秒後にリトライ可能`
      );
    }

    throw new Error(`API Error: ${response.error.message}`);
  }
}

再利用可能な型ガード設計

汎用的で再利用しやすい型ガード関数の設計パターンをご紹介します:

typescript// 汎用的なプロパティ存在チェック型ガード
function hasProperty<T, K extends string>(
  obj: T,
  prop: K
): obj is T & Record<K, unknown> {
  return (
    typeof obj === 'object' && obj !== null && prop in obj
  );
}

// 型付きプロパティチェック
function hasPropertyOfType<T, K extends string, V>(
  obj: T,
  prop: K,
  type: string
): obj is T & Record<K, V> {
  return (
    hasProperty(obj, prop) &&
    typeof (obj as any)[prop] === type
  );
}

汎用型ガードの活用例:

typescript// 様々なオブジェクト型で再利用可能
function processUnknownObject(obj: unknown) {
  if (
    hasPropertyOfType<unknown, 'name', string>(
      obj,
      'name',
      'string'
    )
  ) {
    // obj は { name: string } を含む型として扱われる
    console.log(`名前: ${obj.name}`);
  }

  if (
    hasPropertyOfType<unknown, 'age', number>(
      obj,
      'age',
      'number'
    )
  ) {
    // obj は { age: number } を含む型として扱われる
    console.log(`年齢: ${obj.age}歳`);
  }

  if (
    hasProperty(obj, 'skills') &&
    Array.isArray(obj.skills)
  ) {
    // 配列かどうかのチェックも組み合わせ可能
    console.log(`スキル数: ${obj.skills.length}`);
  }
}

配列要素の型ガードパターン:

typescript// 配列の全要素が特定の型かチェック
function isArrayOf<T>(
  arr: unknown[],
  typeGuard: (item: unknown) => item is T
): arr is T[] {
  return arr.every(typeGuard);
}

// 文字列配列かどうかのチェック
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function processStringArray(data: unknown) {
  if (Array.isArray(data) && isArrayOf(data, isString)) {
    // data は string[] として安全に処理可能
    data.forEach((str) => console.log(str.toUpperCase()));
  }
}

アサーション関数:asserts キーワード

assertsキーワードを使用したアサーション関数は、条件が満たされない場合に例外を投げることで型縮小を実現する手法です。バリデーション処理と型安全性を同時に確保できる強力な機能ですね。

void 型を返す型縮小

基本的なアサーション関数の構文を確認しましょう:

typescript// 基本的なアサーション関数
function assertIsString(
  value: unknown
): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error(`Expected string, got ${typeof value}`);
  }
}

// 使用例
function processStringValue(input: unknown) {
  assertIsString(input);
  // この行以降、input は string型として扱われる
  console.log(input.toUpperCase());
  console.log(`文字数: ${input.length}`);
}

null・undefined チェックのアサーション関数:

typescript// null・undefined チェック用アサーション
function assertNotNull<T>(
  value: T | null | undefined
): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error('Value must not be null or undefined');
  }
}

// 配列要素の存在チェック
function assertArrayHasElements<T>(
  arr: T[]
): asserts arr is [T, ...T[]] {
  if (arr.length === 0) {
    throw new Error('Array must have at least one element');
  }
}

実践的な使用例:

typescriptfunction processUserData(userData: unknown) {
  // データの存在確認
  assertNotNull(userData);

  // オブジェクトかどうかの確認
  assertIsObject(userData);

  // 必要なプロパティの存在と型の確認
  assertHasProperty(userData, 'id');
  assertIsString(userData.id);

  assertHasProperty(userData, 'name');
  assertIsString(userData.name);

  // この段階で userData は安全に使用可能
  console.log(`ユーザーID: ${userData.id}`);
  console.log(`ユーザー名: ${userData.name}`);
}

// 補助的なアサーション関数
function assertIsObject(
  value: unknown
): asserts value is Record<string, unknown> {
  if (typeof value !== 'object' || value === null) {
    throw new Error('Expected object');
  }
}

function assertHasProperty<
  T extends Record<string, unknown>,
  K extends string
>(obj: T, prop: K): asserts obj is T & Record<K, unknown> {
  if (!(prop in obj)) {
    throw new Error(`Expected property '${prop}' to exist`);
  }
}

例外処理との組み合わせ

アサーション関数とエラーハンドリングを組み合わせた実践的なパターンを見てみましょう:

typescript// カスタムエラークラス
class ValidationError extends Error {
  constructor(
    message: string,
    public field: string,
    public value: unknown
  ) {
    super(message);
    this.name = 'ValidationError';
  }
}

// 詳細なエラー情報を含むアサーション関数
function assertIsEmail(
  value: unknown
): asserts value is string {
  if (typeof value !== 'string') {
    throw new ValidationError(
      `Expected email to be string, got ${typeof value}`,
      'email',
      value
    );
  }

  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRegex.test(value)) {
    throw new ValidationError(
      `Invalid email format: ${value}`,
      'email',
      value
    );
  }
}

function assertIsAge(
  value: unknown
): asserts value is number {
  if (typeof value !== 'number') {
    throw new ValidationError(
      `Expected age to be number, got ${typeof value}`,
      'age',
      value
    );
  }

  if (value < 0 || value > 150) {
    throw new ValidationError(
      `Age must be between 0 and 150, got ${value}`,
      'age',
      value
    );
  }
}

包括的なバリデーション処理:

typescriptinterface ValidatedUser {
  email: string;
  age: number;
  name: string;
}

function createUser(userData: unknown): ValidatedUser {
  try {
    assertIsObject(userData);

    // email のバリデーション
    assertHasProperty(userData, 'email');
    assertIsEmail(userData.email);

    // age のバリデーション
    assertHasProperty(userData, 'age');
    assertIsAge(userData.age);

    // name のバリデーション
    assertHasProperty(userData, 'name');
    assertIsString(userData.name);

    if (userData.name.trim().length === 0) {
      throw new ValidationError(
        'Name cannot be empty',
        'name',
        userData.name
      );
    }

    // 全ての検証が通過した場合、安全に型変換
    return {
      email: userData.email,
      age: userData.age,
      name: userData.name.trim(),
    };
  } catch (error) {
    if (error instanceof ValidationError) {
      console.error(
        `バリデーションエラー in ${error.field}: ${error.message}`
      );
    }
    throw error;
  }
}

バリデーションライブラリとの活用

実際の開発では、バリデーションライブラリと組み合わせてアサーション関数を作成することが多いです:

typescript// Zodライブラリを想定した例
import { z } from 'zod';

// スキーマ定義
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1).max(100),
  email: z.string().email(),
  age: z.number().int().min(0).max(150),
  roles: z.array(z.enum(['user', 'admin', 'moderator'])),
});

type User = z.infer<typeof UserSchema>;

// Zodスキーマを使ったアサーション関数
function assertIsValidUser(
  value: unknown
): asserts value is User {
  try {
    UserSchema.parse(value);
  } catch (error) {
    if (error instanceof z.ZodError) {
      const details = error.errors
        .map(
          (err) => `${err.path.join('.')}: ${err.message}`
        )
        .join(', ');
      throw new Error(`User validation failed: ${details}`);
    }
    throw error;
  }
}

API レスポンスの処理での活用:

typescriptasync function fetchUser(userId: string): Promise<User> {
  const response = await fetch(`/api/users/${userId}`);

  if (!response.ok) {
    throw new Error(
      `HTTP ${response.status}: ${response.statusText}`
    );
  }

  const data = await response.json();

  // レスポンスデータのバリデーション
  assertIsValidUser(data);

  // この行以降、data は User型として安全に使用可能
  return data;
}

// 使用例
async function displayUser(userId: string) {
  try {
    const user = await fetchUser(userId);
    console.log(`ユーザー: ${user.name} (${user.email})`);
    console.log(`権限: ${user.roles.join(', ')}`);
  } catch (error) {
    console.error('ユーザーの取得に失敗:', error.message);
  }
}

実際の開発での使い分け

ここまで 4 つの型縮小パターンを詳しく見てきましたが、実際の開発ではどのように使い分けるのが良いでしょうか。それぞれの特徴と適用場面を整理してみましょう。

以下の図で、各パターンの選択指針を確認できます:

mermaidflowchart TD
  start["型縮小が必要な状況"] --> check1{"オブジェクトの<br/>プロパティで判定?"};
  check1 --|Yes|--> in_op["in オペレータ"];
  check1 --|No|--> check2{"クラスの<br/>インスタンス判定?"};
  check2 --|Yes|--> instanceofOp["instanceof オペレータ"];
  check2 --|No|--> check3{"複雑な条件や<br/>再利用が必要?"};
  check3 --|Yes|--> is_guard["is 型ガード"];
  check3 --|No|--> check4{"バリデーションと<br/>例外処理が必要?"};
  check4 --|Yes|--> assertsOp["asserts アサーション"];
  check4 --|No|--> other["その他の手法<br/>(typeof, Array.isArray など)"];

パターン別使い分けガイド

パターン適用場面メリット注意点
in オペレータUnion 型のオブジェクトで特定プロパティの存在で判定シンプルで直感的、TypeScript が自動推論optional プロパティでは効果薄、文字列リテラル必須
instanceofクラスベースの型判定、継承関係の処理継承チェーンに対応、オブジェクトの出自が明確プリミティブ型不可、クラス設計への依存
is 型ガード複雑な条件、再利用可能な型チェック柔軟性が高い、ロジックの再利用可能実装コストが高め、条件の妥当性は自己責任
assertsバリデーション処理、例外的状況での型保証型安全性とランタイムチェックの統合例外処理が必要、パフォーマンスへの配慮要

実践的な組み合わせパターン

実際の開発では、複数の型縮小手法を組み合わせて使用することが多いです:

typescript// 複数パターンの組み合わせ例
class ApiService {
  // アサーション関数でデータの妥当性を保証
  private assertValidResponse(
    data: unknown
  ): asserts data is {
    status: string;
    result: unknown;
  } {
    if (typeof data !== 'object' || data === null) {
      throw new Error('Invalid response format');
    }

    if (!('status' in data) || !('result' in data)) {
      throw new Error('Missing required response fields');
    }
  }

  // 型ガードで詳細な型判定
  private isErrorResponse(response: {
    status: string;
    result: unknown;
  }): response is {
    status: 'error';
    result: { message: string; code: number };
  } {
    return (
      response.status === 'error' &&
      typeof response.result === 'object' &&
      response.result !== null &&
      'message' in response.result &&
      'code' in response.result
    );
  }

  private isSuccessResponse<T>(response: {
    status: string;
    result: unknown;
  }): response is { status: 'success'; result: T } {
    return response.status === 'success';
  }

  async processApiCall<T>(url: string): Promise<T> {
    const response = await fetch(url);
    const data = await response.json();

    // まずアサーション関数で基本構造を確認
    this.assertValidResponse(data);

    // 次に型ガードで詳細な型判定
    if (this.isErrorResponse(data)) {
      throw new Error(
        `API Error ${data.result.code}: ${data.result.message}`
      );
    } else if (this.isSuccessResponse<T>(data)) {
      return data.result;
    } else {
      throw new Error('Unexpected response format');
    }
  }
}

フォーム処理での実践例:

typescript// フォームデータ処理での型縮小活用
interface FormData {
  name: string;
  email: string;
  age?: number;
  preferences?: {
    newsletter: boolean;
    theme: 'light' | 'dark';
  };
}

class FormProcessor {
  // アサーション関数で必須フィールドをチェック
  private assertRequiredFields(
    data: unknown
  ): asserts data is {
    name: unknown;
    email: unknown;
  } {
    if (typeof data !== 'object' || data === null) {
      throw new Error('Form data must be an object');
    }

    if (!('name' in data) || !('email' in data)) {
      throw new Error('Name and email are required');
    }
  }

  // 型ガードでオプショナルフィールドをチェック
  private hasPreferences(
    data: any
  ): data is { preferences: unknown } {
    return (
      'preferences' in data &&
      typeof data.preferences === 'object'
    );
  }

  processForm(rawData: unknown): FormData {
    // 必須フィールドの確認
    this.assertRequiredFields(rawData);

    // 基本フィールドの型チェック
    if (
      typeof rawData.name !== 'string' ||
      typeof rawData.email !== 'string'
    ) {
      throw new Error('Name and email must be strings');
    }

    const result: FormData = {
      name: rawData.name,
      email: rawData.email,
    };

    // in オペレータでオプショナルフィールドをチェック
    if (
      'age' in rawData &&
      typeof rawData.age === 'number'
    ) {
      result.age = rawData.age;
    }

    // 型ガードでネストしたオブジェクトを処理
    if (this.hasPreferences(rawData)) {
      const prefs = rawData.preferences;
      if (
        typeof prefs === 'object' &&
        prefs !== null &&
        'newsletter' in prefs &&
        'theme' in prefs
      ) {
        result.preferences = {
          newsletter: Boolean(prefs.newsletter),
          theme: prefs.theme === 'dark' ? 'dark' : 'light',
        };
      }
    }

    return result;
  }
}

パフォーマンスとメンテナンス性の考慮

型縮小パターンを選択する際は、パフォーマンスとメンテナンス性も重要な要素です:

パフォーマンス面の考慮点

  • inオペレータとinstanceofは実行時のコストが低い
  • 複雑な型ガード関数は条件によってはコストが高くなる
  • アサーション関数は例外処理のコストを考慮する必要がある

メンテナンス性の考慮点

  • シンプルな条件ではininstanceofを優先
  • 複雑なロジックは型ガード関数にまとめて再利用性を高める
  • バリデーション要件が厳しい場面ではアサーション関数が有効

効率的な型縮小の実装例:

typescript// パフォーマンスを意識した型縮小
class OptimizedTypeNarrowing {
  // 頻繁に呼ばれる処理では軽量な判定を優先
  private fastTypeCheck(
    value: unknown
  ): value is string | number {
    const type = typeof value;
    return type === 'string' || type === 'number';
  }

  // 複雑な判定が必要な場合のみ詳細チェック
  private detailedTypeCheck(
    value: unknown
  ): value is ComplexType {
    // 軽量チェックで早期リターン
    if (!this.fastTypeCheck(value)) {
      return false;
    }

    // 必要な場合のみ詳細なチェック
    return this.performDetailedValidation(value);
  }

  private performDetailedValidation(
    value: string | number
  ): value is ComplexType {
    // 詳細なバリデーションロジック
    // ...
    return true; // 簡略化
  }
}

interface ComplexType {
  // 複雑な型定義
}

まとめ

TypeScript の型縮小は、型安全性と開発体験を大幅に向上させる重要な機能です。本記事で解説した 4 つのパターンを適切に使い分けることで、より堅牢で保守性の高いコードを書くことができますね。

各パターンの要点

  • inオペレータ:オブジェクトのプロパティ存在で判定。Union 型の処理に最適
  • instanceofオペレータ:クラスベースの型判定。継承関係に強い
  • is型ガード:複雑な条件や再利用可能な型チェック。柔軟性が高い
  • assertsアサーション:バリデーションと型安全性を統合。例外処理との組み合わせ

実際の開発では、これらの手法を組み合わせて使用することで、型安全性を保ちながら実用的なアプリケーションを構築できます。プロジェクトの要件や処理の複雑さに応じて、最適なパターンを選択していきましょう。

型縮小をマスターすることで、TypeScript の真の力を引き出し、より良いソフトウェア開発体験を得られることでしょう。

関連リンク