TypeScript型ガードを使った業務パターン実装ガイド|実務コード解説

未分類

TypeScript型ガードを使った業務パターン実装ガイド|実務コード解説

はじめに

TypeScriptを使った開発で、型安全性を保ちながら柔軟にコードを書くことは重要な課題です。特に外部APIからのレスポンスやユーザー入力など、実行時に型が確定しないデータを扱う場面では、型ガード(Type Guard)が非常に役立ちます。

本記事では、TypeScriptの型ガードについて、単なる理論的な説明ではなく、実際の業務で出くわすシナリオを基にした実装パターンを紹介します。

型ガードとは|簡易的な解説

型ガードは、TypeScriptで変数の型を絞り込む(型の範囲を限定する)ための仕組みです。実行時に特定の条件を検査して、その後のコードブロック内でTypeScriptコンパイラが型を正確に認識できるようにします。

例えば、引数としてstring | number型の値を受け取った関数内で、その値が具体的にどちらの型なのかを判定し、以降のコード内でより具体的な型として扱うことができます。

// 基本的な例
function processValue(value: string | number) {
  if (typeof value === 'string') {
    // この分岐内ではvalueはstring型として認識される
    console.log(value.toUpperCase());
  } else {
    // この分岐内ではvalueはnumber型として認識される
    console.log(value.toFixed(2));
  }
}

実務では、この型ガードをより複雑で実践的なシナリオに応用します。

業務でのユースケース

ユースケース1:API レスポンスの型検証

最も一般的なユースケースは、外部APIからのレスポンスの検証です。バックエンドから返されるデータが常に予想通りの形式とは限りません。

// APIから返される可能性のあるレスポンス型
interface SuccessResponse {
  status: 'success';
  data: {
    id: number;
    name: string;
    email: string;
  };
}

interface ErrorResponse {
  status: 'error';
  errorCode: string;
  message: string;
}

type ApiResponse = SuccessResponse | ErrorResponse;

// 型ガード関数
function isSuccessResponse(response: ApiResponse): response is SuccessResponse {
  return response.status === 'success';
}

// 使用例
async function fetchUserData(userId: number) {
  const response: ApiResponse = await fetch(`/api/users/${userId}`).then(r => r.json());
  
  if (isSuccessResponse(response)) {
    // この分岐では response.data が利用可能
    console.log(`ユーザー名: ${response.data.name}`);
    return response.data;
  } else {
    // エラーレスポンスの処理
    console.error(`エラー: ${response.errorCode} - ${response.message}`);
    throw new Error(response.message);
  }
}

ユースケース2:フォームデータの複合バリデーション

ユーザー入力のバリデーションでは、複数の条件を組み合わせて型を絞り込む必要があります。

interface PremiumUserData {
  userType: 'premium';
  memberId: string;
  subscriptionEnd: Date;
  billingAddress: string;
}

interface FreeUserData {
  userType: 'free';
  userId: string;
}

type UserData = PremiumUserData | FreeUserData;

// 複合条件の型ガード
function isPremiumUser(user: UserData): user is PremiumUserData {
  return (
    user.userType === 'premium' &&
    'memberId' in user &&
    'subscriptionEnd' in user &&
    new Date(user.subscriptionEnd) > new Date()
  );
}

// 実務での使用
function applyDiscount(user: UserData): number {
  if (isPremiumUser(user)) {
    console.log(`プレミアム会員 ${user.memberId} に割引を適用`);
    return 0.2; // 20% 割引
  } else {
    console.log(`無料会員 ${user.userId} は割引対象外`);
    return 0;
  }
}

ユースケース3:イベントハンドラの型指定

React や Vue などのフレームワークでイベントを扱う際、異なる種類のイベントオブジェクトを統一的に処理する場面があります。

type FormEvent = React.FormEvent | React.ChangeEvent;

function isFormSubmitEvent(event: FormEvent): event is React.FormEvent {
  return event.type === 'submit';
}

function isInputChangeEvent(event: FormEvent): event is React.ChangeEvent {
  return event.type === 'change';
}

function handleFormInteraction(event: FormEvent) {
  if (isFormSubmitEvent(event)) {
    event.preventDefault();
    console.log('フォーム送信を処理');
  } else if (isInputChangeEvent(event)) {
    console.log(`入力値が変更されました: ${event.currentTarget.value}`);
  }
}

実装コード|実務パターン集

パターン1:Discriminated Union による型ガード

業務では、複数の異なるレスポンス型を扱うことが多くあります。discriminated union(判別ユニオン)は、共通のプロパティに基づいて型を絞り込む最も効果的な方法です。

// データベースクエリの結果型
type QueryResult = 
  | { status: 'success'; records: any[]; count: number }
  | { status: 'error'; errorCode: string; details: string }
  | { status: 'timeout'; retryAfter: number };

function processQueryResult(result: QueryResult): void {
  switch (result.status) {
    case 'success':
      console.log(`${result.count} 件のレコードを取得しました`);
      result.records.forEach(record => console.log(record));
      break;
    
    case 'error':
      console.error(`クエリエラー [${result.errorCode}]: ${result.details}`);
      break;
    
    case 'timeout':
      console.warn(`タイムアウト。${result.retryAfter}秒後に再試行してください`);
      break;
  }
}

パターン2:in 演算子を使った型ガード

オブジェクトが特定のプロパティを持つかどうかで型を判定する場合です。レガシーなAPIとの連携時に役立ちます。

interface OldApiResponse {
  result: string;
  data: any;
}

interface NewApiResponse {
  statusCode: number;
  payload: any;
  timestamp: string;
}

type ApiResponseType = OldApiResponse | NewApiResponse;

function handleApiResponse(response: ApiResponseType) {
  if ('statusCode' in response && 'timestamp' in response) {
    // 新しいAPI形式
    if (response.statusCode === 200) {
      console.log(`成功(新形式): ${response.payload}`);
    }
  } else if ('result' in response && 'data' in response) {
    // 古いAPI形式
    if (response.result === 'OK') {
      console.log(`成功(旧形式): ${response.data}`);
    }
  }
}

パターン3:カスタム型ガード関数

複数の条件を組み合わせた複雑な検証は、専用の型ガード関数に切り出すのが実務的です。

interface Product {
  id: string;
  name: string;
  price: number;
  inventory?: number;
  tags?: string[];
}

interface DigitalProduct extends Product {
  downloadUrl: string;
  licenseKey: string;
}

interface PhysicalProduct extends Product {
  weight: number;
  dimensions: {
    length: number;
    width: number;
    height: number;
  };
}

// カスタム型ガード関数
function isDigitalProduct(product: Product): product is DigitalProduct {
  return (
    'downloadUrl' in product &&
    'licenseKey' in product &&
    typeof (product as any).downloadUrl === 'string' &&
    typeof (product as any).licenseKey === 'string'
  );
}

function isPhysicalProduct(product: Product): product is PhysicalProduct {
  return (
    'weight' in product &&
    'dimensions' in product &&
    typeof (product as any).weight === 'number'
  );
}

// 在庫管理システムでの使用
function updateInventory(product: Product, quantity: number): void {
  if (isPhysicalProduct(product)) {
    // 物理商品のみ在庫管理が必要
    const newInventory = (product.inventory || 0) - quantity;
    if (newInventory < 0) {
      console.error(`在庫不足: ${product.name}`);
    } else {
      console.log(`${product.name} の在庫を更新: ${newInventory}個`);
    }
  } else if (isDigitalProduct(product)) {
    // デジタル商品は在庫チェック不要
    console.log(`デジタル商品 ${product.name} をダウンロード提供`);
  }
}

パターン4:Array.filter での型ガード

配列処理で特定の条件のみを抽出する際、型ガードは強力なツールになります。

type Order = 
  | { type: 'completed'; orderId: string; totalAmount: number; deliveryDate: Date }
  | { type: 'pending'; orderId: string; estimatedDate: Date }
  | { type: 'cancelled'; orderId: string; reason: string };

function isCompletedOrder(order: Order): order is Order & { type: 'completed' } {
  return order.type === 'completed';
}

const orders: Order[] = [
  { type: 'completed', orderId: 'ORD001', totalAmount: 15000, deliveryDate: new Date('2024-01-15') },
  { type: 'pending', orderId: 'ORD002', estimatedDate: new Date('2024-02-01') },
  { type: 'completed', orderId: 'ORD003', totalAmount: 8500, deliveryDate: new Date('2024-01-20') },
  { type: 'cancelled', orderId: 'ORD004', reason: 'Customer request' },
];

// 完了した注文のみを抽出して集計
const completedOrders = orders.filter(isCompletedOrder);
const totalRevenue = completedOrders.reduce((sum, order) => sum + order.totalAmount, 0);
console.log(`売上合計: ¥${totalRevenue}`);

よくある応用パターン

応用1:非同期処理との組み合わせ

Promise の解決結果に対する型ガードです。実務で最も頻繁に出現するパターンの一つです。

type ApiResult = 
  | { ok: true; data: T }
  | { ok: false; errorMessage: string; errorCode: number };

async function fetchAndProcess(url: string): Promise {
  try {
    const response = await fetch(url);
    const data: ApiResult = await response.json();
    
    if (data.ok) {
      return data.data; // 型安全にT型として返される
    } else {
      console.error(`API Error: [${data.errorCode}] ${data.errorMessage}`);
      return null;
    }
  } catch (error) {
    console.error('Network error:', error);
    return null;
  }
}

// 使用例
interface UserProfile {
  id: number;
  name: string;
  role: 'admin' | 'user';
}

const user = await fetchAndProcess('/api/user/me');
if (user) {
  console.log(`ユーザー: ${user.name} (${user.role})`);
}

応用2:クラスベースの型ガード

instanceof を使った型ガードは、クラスベースの設計で役立ちます。

abstract class CustomError extends Error {
  abstract statusCode: number;
  abstract isOperational: boolean;
}

class ValidationError extends CustomError {
  statusCode = 400;
  isOperational = true;
  
  constructor(public field: string, message: string) {
    super(message);
    Object.setPrototypeOf(this, ValidationError.prototype);
  }
}

class DatabaseError extends CustomError {
  statusCode = 500;
  isOperational = false;
  
  constructor(message: string) {
    super(message);
    Object.setPrototypeOf(this, DatabaseError.prototype);
  }
}

// 型ガード関数
function isValidationError(error: Error): error is ValidationError {
  return error instanceof ValidationError;
}

// エラーハンドリング
function handleError(error: CustomError) {
  if (isValidationError(error)) {
    console.log(`バリデーションエラー [${error.field}]: ${error.message}`);
  } else if (error instanceof DatabaseError) {
    console.error(`データベースエラー: ${error.message}`);
    // オペレーショナルでないエラーは監視ツールに通知
  }
}

応用3:関数のオーバーロードと型ガード

関数のオーバーロードと組み合わせると、より柔軟なAPI設計が可能になります。

// オーバーロード宣言
function getUserData(id: number): Promise<{ id: number; name: string }>;
function getUserData(email: string): Promise<{ id: number; email: string }>;

// 実装
async function getUserData(identifier: number | string) {
  // 型ガードで引数の型を判定
  const isNumeric = typeof identifier === 'number';
  const endpoint = isNumeric ? `/api/users/${identifier}` : `/api/users/email/${identifier}`;
  
  const response = await fetch(endpoint);
  return response.json();
}

// 型安全に呼び出し可能
const userById = await getUserData(123);
const userByEmail = await getUserData('user@example.com');

注意点と落とし穴

注意点1:型ガード関数の正確性

型ガード関数は単なるヘルパーではなく、TypeScriptコンパイラの型推論に直接影響します。実装に誤りがあると、型安全性が崩壊します。

// ❌ 危険な実装
function isSuspiciousOrder(order: any): order is Order {
  // チェックが不完全
  return 'orderId' in order;
}

// ✅ 正しい実装
function isSuspiciousOrder(order: any): order is Order {
  return (
    'orderId' in order &&
    'status' in order &&
    typeof order.orderId === 'string' &&
    typeof order.status === 'string'
  );
}

注意点2:型ガード後の型エイリアス

型ガード後の型は必ず明確にしておくべきです。暗黙的な推論に頼るとバグが生まれやすいです。

// ❌ 型が不明確
const activeUsers = users.filter(u => 'premiumEndDate' in u && new Date(u.premiumEndDate) > new Date());
// activeUsersの型は何か不明確

// ✅ 明確にする
const isPremiumUser = (u: User): u is PremiumUser => 
  'premiumEndDate' in u && new Date(u.premiumEndDate) > new Date();

const activeUsers = users.filter(isPremiumUser);
// activeUsersはPremiumUser[]型で確実

注意点3:null/undefined チェック

型ガードでは null や undefined の処理を忘れがちです。特に optional properties を扱う時注意が必要です。

// ❌ nullチェックが不十分
function processResponse(data: any) {
  if (data.payload) {
    console.log(data.payload.value); // payloadが存在してもvalueが存在しない可能性
  }
}

// ✅ 厳密なチェック
function processResponse(data: any) {
  if (
    data &&
    data.payload &&
    typeof data.payload === 'object' &&
    'value' in data.payload &&
    data.payload.value !== null &&
    data.payload.value !== undefined
  ) {
    console.log(data.payload.value);
  }
}

まとめ

TypeScript の型ガードは、単なる構文機能ではなく、実務アプリケーションの信頼性と保守性を大幅に向上させるツールです。

本記事で紹介した主なポイントは以下の通りです:

  • API レスポンス検証:外部データの型安全性を確保する最重要用途
  • 判別ユニオン:複数の型を扱うときの定番パターン
  • カスタム型ガード関数:複合条件の検証を切り出して再利用可能にする
  • 非同期処理との組み合わせ:Promise の結果に対する堅牢な処理
  • 型安全性の厳密さ:実装の誤りが型システムの根拠を揺るがす可能性を意識する

特に大規模なプロジェクトでは、型ガードの正確な実装が後々のバグ修正や機能追加の際に大きな効果を発揮します。面倒だと感じても、丁寧に型ガード関数を整備することを強くお勧めします。

実務経験を積むにつれて、どのシナリオでどの型ガードのパターンを適用するかが自然と判断できるようになります。本記事のコード例を参考に、プロジェクトに合わせた型ガード戦略を構築してください。

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