TypeScript typeof演算子の実務活用法|型安全なコード実装パターン

未分類

TypeScript typeof演算子の実務活用法|型安全なコード実装パターン

\n\n

\n

はじめに:typeof演算子の基本的な役割

\n

TypeScriptを使う際、「このデータは本当に期待した型なのか」という確認作業は日常的に発生します。特にAPIからのレスポンスやユーザー入力など、外部から受け取るデータの型安全性を確保することは、バグ防止の観点から非常に重要です。

\n

typeof演算子は、実行時にデータの型を判定し、その結果に基づいて処理を分岐させることができるJavaScriptの機能です。TypeScriptではこれを活用することで、コンパイル時の型チェックと実行時の動的な型判定を組み合わせ、堅牢で柔軟なコードを実装できます。

\n

本記事では、私が実務で実際に活用している具体的なパターンを、業務背景を含めて解説していきます。

\n

\n\n

\n

簡易的な解説:typeof演算子とは

\n

typeof演算子は、オペランドのデータ型を文字列で返します。JavaScriptの基本的な機能ですが、TypeScriptでは「型ガード」として使うことで、特定の型に絞り込んだ処理を実装できます。

\n\n

基本的な使い方

\n

// 基本的な例\nconst value: string | number = \"hello\";\n\nif (typeof value === \"string\") {\n  // この中ではvalueはstring型として扱われる\n  console.log(value.toUpperCase());\n} else if (typeof value === \"number\") {\n  // この中ではvalueはnumber型として扱われる\n  console.log(value.toFixed(2));\n}\n

\n\n

typeof演算子が返す値は以下の通りです:

\n

    \n

  • \”string\” – 文字列型
  • \n

  • \”number\” – 数値型
  • \n

  • \”boolean\” – 真偽値型
  • \n

  • \”undefined\” – 未定義
  • \n

  • \”function\” – 関数
  • \n

  • \”object\” – オブジェクト(null含む)
  • \n

  • \”symbol\” – シンボル型
  • \n

  • \”bigint\” – 大整数型
  • \n

\n\n

重要な注意点として、typeof nullは\”object\”を返します。これはJavaScriptの歴史的な仕様で、nullをチェックする際は別途対応が必要です。

\n

\n\n

\n

業務でのユースケース:実務で頻出する場面

\n\n

ユースケース1:API レスポンスの型安全性確保

\n

最初のプロジェクトのイメージとして、ECサイトのバックエンド提供データを処理する場面を想定します。外部APIや他チームが管理するエンドポイントからのレスポンスは、時に予期しない構造で返ってくることがあります。

\n\n

例えば、商品一覧APIが正常系では配列を返しますが、エラー時には文字列のメッセージを返すかもしれません。このような不確定な状況で、typeof を活用して柔軟に処理を分岐させることができます。

\n\n

ユースケース2:フォームバリデーション

\n

複数の入力フォームがある画面で、ユーザーの入力値が想定の型かどうかを動的に確認する必要があります。フロントエンドでもTypeScriptを使用している場合、入力値の型チェックは非常に重要です。

\n\n

ユースケース3:ライブラリの互換性処理

\n

複数のバージョンのライブラリを同時に利用する場合や、ユーザーの環境によって提供されるオブジェクトの構造が異なる場合があります。typeof を使い、存在する機能に応じた条件分岐を実装します。

\n

\n\n

\n

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

\n\n

パターン1: API レスポンス処理(型安全性の確保)

\n

実務で最も頻繁に登場するパターンです。外部APIから返却されるデータが、期待した型かどうかを確認し、適切に処理します。

\n\n

// APIレスポンスの型定義\ninterface Product {\n  id: number;\n  name: string;\n  price: number;\n  description?: string;\n}\n\ninterface ApiError {\n  error: string;\n  statusCode: number;\n}\n\n// APIからのレスポンス(型が不確定)\nasync function fetchProductList(): Promise {\n  try {\n    const response = await fetch('/api/products');\n    const data = await response.json();\n    \n    // typeof による型チェック\n    if (data === null || data === undefined) {\n      return null;\n    }\n    \n    // 配列かどうかで判定\n    if (Array.isArray(data)) {\n      return data as Product[];\n    }\n    \n    // エラーオブジェクトの判定\n    if (typeof data === 'object' && 'error' in data) {\n      return data as ApiError;\n    }\n    \n    return null;\n  } catch (error) {\n    console.error('Fetch error:', error);\n    return null;\n  }\n}\n\n// 呼び出し側での処理\nasync function displayProducts() {\n  const result = await fetchProductList();\n  \n  if (result === null) {\n    console.log('データがありません');\n    return;\n  }\n  \n  if (Array.isArray(result)) {\n    // Product[] として処理\n    result.forEach(product => {\n      console.log(`${product.name}: ${product.price}円`);\n    });\n  } else if (typeof result === 'object' && 'error' in result) {\n    // ApiError として処理\n    console.error(`エラー [${result.statusCode}]: ${result.error}`);\n  }\n}\n

\n\n

パターン2:複雑なユニオン型の型絞り込み

\n

複数の異なる型を返す可能性がある関数で、typeof を用いて各型ごとの処理を実装します。

\n\n

// 複数の戻り値の型を定義\ntype UserData = {\n  type: 'user';\n  id: number;\n  name: string;\n  email: string;\n};\n\ntype GuestData = {\n  type: 'guest';\n  sessionId: string;\n};\n\ntype ErrorData = {\n  type: 'error';\n  message: string;\n  code: number;\n};\n\ntype AuthResult = UserData | GuestData | ErrorData;\n\n// 認証情報を処理する関数\nfunction handleAuthResult(result: AuthResult): void {\n  // 最初に type フィールドで判定\n  if (result.type === 'user') {\n    // UserData として処理\n    console.log(`ユーザー: ${result.name} (${result.email})`);\n    sendAnalytics('login_user', { userId: result.id });\n  } else if (result.type === 'guest') {\n    // GuestData として処理\n    console.log(`ゲストセッション: ${result.sessionId}`);\n    sendAnalytics('login_guest', { sessionId: result.sessionId });\n  } else if (result.type === 'error') {\n    // ErrorData として処理\n    console.error(`認証エラー [${result.code}]: ${result.message}`);\n    logError('auth_failed', result);\n  }\n}\n\nfunction sendAnalytics(event: string, data: Record): void {\n  // 分析処理\n}\n\nfunction logError(event: string, data: ErrorData): void {\n  // エラー記録処理\n}\n

\n\n

パターン3:フォーム入力値の動的なバリデーション

\n

複数フィールドを持つフォームで、各フィールドの入力値が期待した型かどうかをチェックします。

\n\n

// フォーム入力値の型\ninterface FormInput {\n  [key: string]: unknown;\n}\n\n// バリデーションルール\ninterface ValidationRule {\n  type: 'string' | 'number' | 'boolean' | 'email';\n  required: boolean;\n  minLength?: number;\n  maxLength?: number;\n}\n\nconst formRules: Record = {\n  username: { type: 'string', required: true, minLength: 3, maxLength: 20 },\n  age: { type: 'number', required: true },\n  email: { type: 'email', required: true },\n  newsletter: { type: 'boolean', required: false },\n};\n\n// バリデーション実装\nfunction validateForm(formData: FormInput): { valid: boolean; errors: Record } {\n  const errors: Record = {};\n  \n  for (const [fieldName, rule] of Object.entries(formRules)) {\n    const value = formData[fieldName];\n    \n    // 必須チェック\n    if (rule.required && (value === undefined || value === null || value === '')) {\n      errors[fieldName] = '必須項目です';\n      continue;\n    }\n    \n    // 空白許可の場合はスキップ\n    if (!rule.required && (value === undefined || value === null || value === '')) {\n      continue;\n    }\n    \n    // 型チェック\n    if (rule.type === 'email') {\n      if (typeof value !== 'string' || !isValidEmail(value)) {\n        errors[fieldName] = '有効なメールアドレスではありません';\n      }\n    } else if (rule.type === 'string') {\n      if (typeof value !== 'string') {\n        errors[fieldName] = '文字列である必要があります';\n        continue;\n      }\n      if (rule.minLength && value.length < rule.minLength) {\n        errors[fieldName] = `${rule.minLength}文字以上である必要があります`;\n      }\n      if (rule.maxLength && value.length > rule.maxLength) {\n        errors[fieldName] = `${rule.maxLength}文字以下である必要があります`;\n      }\n    } else if (rule.type === 'number') {\n      if (typeof value !== 'number' || isNaN(value)) {\n        errors[fieldName] = '数値である必要があります';\n      }\n    } else if (rule.type === 'boolean') {\n      if (typeof value !== 'boolean') {\n        errors[fieldName] = '真偽値である必要があります';\n      }\n    }\n  }\n  \n  return {\n    valid: Object.keys(errors).length === 0,\n    errors,\n  };\n}\n\nfunction isValidEmail(email: string): boolean {\n  const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n  return emailRegex.test(email);\n}\n\n// 使用例\nconst formInput: FormInput = {\n  username: 'john_doe',\n  age: 30,\n  email: 'john@example.com',\n  newsletter: true,\n};\n\nconst result = validateForm(formInput);\nif (result.valid) {\n  console.log('フォームは有効です');\n} else {\n  console.log('エラー:', result.errors);\n}\n

\n\n

パターン4:条件付きデータ変換(複雑な業務ロジック)

\n

異なる型の入力値に対して、適切な変換処理を施すパターンです。決済処理などの重要な処理で使用されます。

\n\n

// 決済情報の型\ntype PaymentInfo = \n  | { method: 'credit'; cardNumber: string; cvv: string }\n  | { method: 'bank'; accountNumber: string; bankCode: string }\n  | { method: 'wallet'; walletId: string };\n\ntype PaymentAmount = number | string;\n\n// 決済処理\nfunction processPayment(amount: PaymentAmount, paymentInfo: PaymentInfo): Promise {\n  // 金額の型チェックと正規化\n  let normalizedAmount: number;\n  \n  if (typeof amount === 'string') {\n    // カンマ区切りの文字列を数値に変換\n    const parsed = parseInt(amount.replace(/,/g, ''), 10);\n    if (isNaN(parsed) || parsed <= 0) {\n      throw new Error('無効な金額です');\n    }\n    normalizedAmount = parsed;\n  } else if (typeof amount === 'number') {\n    if (amount <= 0) {\n      throw new Error('金額は正の数である必要があります');\n    }\n    normalizedAmount = amount;\n  } else {\n    throw new Error('金額は数値または文字列である必要があります');\n  }\n  \n  // 決済方法に応じた処理\n  switch (paymentInfo.method) {\n    case 'credit':\n      return processCreditPayment(normalizedAmount, paymentInfo.cardNumber, paymentInfo.cvv);\n    case 'bank':\n      return processBankTransfer(normalizedAmount, paymentInfo.accountNumber, paymentInfo.bankCode);\n    case 'wallet':\n      return processWalletPayment(normalizedAmount, paymentInfo.walletId);\n    default:\n      const exhaustiveCheck: never = paymentInfo;\n      return Promise.reject(new Error(`未対応の決済方法: ${exhaustiveCheck}`));\n  }\n}\n\nasync function processCreditPayment(amount: number, cardNumber: string, cvv: string): Promise {\n  // クレジットカード処理\n  console.log(`クレジットカード決済: ${amount}円`);\n  return true;\n}\n\nasync function processBankTransfer(amount: number, accountNumber: string, bankCode: string): Promise {\n  // 銀行振込処理\n  console.log(`銀行振込: ${amount}円`);\n  return true;\n}\n\nasync function processWalletPayment(amount: number, walletId: string): Promise {\n  // ウォレット決済処理\n  console.log(`ウォレット決済: ${amount}円`);\n  return true;\n}\n

\n

\n\n

\n

よくある応用パターン:実務での応用事例

\n\n

応用パターン1:ロギングと型情報の記録

\n

デバッグやモニタリングの際に、変数の型情報を記録することは重要です。typeof を利用した汎用的なログ関数を実装できます。

\n\n

// 詳細なロギング関数\nfunction logWithType(label: string, value: unknown): void {\n  let typeInfo = typeof value;\n  let displayValue = value;\n  \n  // より詳細な型情報を取得\n  if (value === null) {\n    typeInfo = 'null';\n  } else if (Array.isArray(value)) {\n    typeInfo = `array[${(value as unknown[]).length}]`;\n  } else if (value instanceof Date) {\n    typeInfo = 'Date';\n    displayValue = value.toISOString();\n  } else if (value instanceof Map) {\n    typeInfo = `Map[${(value as Map).size}]`;\n  } else if (typeof value === 'object') {\n    typeInfo = `object[${Object.keys(value as Record).length}]`;\n  }\n  \n  console.log(`[${new Date().toISOString()}] ${label} (${typeInfo}):`, displayValue);\n}\n\n// 使用例\nlogWithType('ユーザー情報', { id: 1, name: 'John' });\nlogWithType('エラーメッセージ', 'Something went wrong');\nlogWithType('結果', null);\nlogWithType('リスト', [1, 2, 3]);\n

\n\n

応用パターン2:オブジェクトのマージと型安全性

\n

複数のオブジェクトをマージする場合、各値の型を考慮して適切にマージする処理です。

\n\n

// 設定オブジェクトをマージする関数\nfunction mergeConfig(\n  defaults: Record,\n  overrides: Record\n): Record {\n  const merged: Record = { ...defaults };\n  \n  for (const [key, value] of Object.entries(overrides)) {\n    // nullやundefinedはスキップ\n    if (value === null || value === undefined) {\n      continue;\n    }\n    \n    // オブジェクト同士の場合は深くマージ\n    if (\n      typeof value === 'object' &&\n      typeof merged[key] === 'object' &&\n      merged[key] !== null &&\n      !Array.isArray(value) &&\n      !Array.isArray(merged[key])\n    ) {\n      merged[key] = mergeConfig(\n        merged[key] as Record,\n        value as Record\n      );\n    } else {\n      // その他の場合は上書き\n      merged[key] = value;\n    }\n  }\n  \n  return merged;\n}\n\n// 使用例\nconst defaultConfig = {\n  apiUrl: 'https://api.example.com',\n  timeout: 5000,\n  retries: 3,\n  headers: {\n    'Content-Type': 'application/json',\n  },\n};\n\nconst userConfig = {\n  timeout: 10000,\n  headers: {\n    'Authorization': 'Bearer token123',\n  },\n};\n\nconst finalConfig = mergeConfig(defaultConfig, userConfig);\nconsole.log(finalConfig);\n// 出力: { apiUrl: '...', timeout: 10000, retries: 3, headers: { 'Content-Type': '...', 'Authorization': '...' } }\n

\n\n

応用パターン3:型ガード関数の作成

\n

typeof を活用した再利用可能な型ガード関数を作成し、コード全体で使い回すパターンです。

\n\n

// 型ガード関数群\nfunction isString(value: unknown): value is string {\n  return typeof value === 'string';\n}\n\nfunction isNumber(value: unknown): value is number {\n  return typeof value === 'number' && !isNaN(value);\n}\n\nfunction isBoolean(value: unknown): value is boolean {\n  return typeof value === 'boolean';\n}\n\nfunction isFunction(value: unknown): value is Function {\n  return typeof value === 'function';\n}\n\nfunction isObject(value: unknown): value is Record {\n  return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isStringArray(value: unknown): value is string[] {\n  return Array.isArray(value) && value.every(item => typeof item === 'string');\n}\n\n// ユーティリティ関数の例\nfunction stringify(value: unknown): string {\n  if (isString(value)) {\n    return value;\n  } else if (isNumber(value)) {\n    return value.toString();\n  } else if (isBoolean(value)) {\n    return value ? 'true' : 'false';\n  } else if (value === null) {\n    return 'null';\n  } else if (isObject(value) || Array.isArray(value)) {\n    return JSON.stringify(value);\n  } else if (isFunction(value)) {\n    return '[Function]';\n  }\n  return String(value);\n}\n\n// 使用例\nconsole.log(stringify('hello'));        // 'hello'\nconsole.log(stringify(42));             // '42'\nconsole.log(stringify(true));           // 'true'\nconsole.log(stringify({ id: 1 }));      // '{\"id\":1}'\n

\n

\n\n

\n

注意点:実務で気をつけるべきこと

\n\n

注意点1:typeof null の落とし穴

\n

JavaScriptの歴史的仕様として、typeof null は \”object\” を返します。これは多くの開発者を悩ませるバグの原因になります。

\n\n

// 間違った例\nconst value: unknown = null;\n\nif (typeof value === 'object') {\n  // ここにnullが入ってくることがある!\n  console.log(value.someProperty); // Error: Cannot read property\n}\n\n// 正しい例\nif (value !== null && typeof value === 'object') {\n  console.log(value.someProperty); // 安全\n}\n\n// または\nif (typeof value === 'object' && value !== null) {\n  console.log(value.someProperty); // これでも安全\n}\n

\n\n

注意点2:typeof で配列を判定できない

\n

配列は typeof では ‘object’ として返されるため、Array.isArray() との併用が必要です。

\n\n

// 間違った例\nconst arr = [1, 2, 3];\nif (typeof arr === 'object') {\n  // これは true だが、オブジェクトの場合と区別できない\n}\n\n// 正しい例\nif (Array.isArray(arr)) {\n  console.log('配列です');\n} else if (typeof arr === 'object' && arr !== null) {\n  console.log('オブジェクトです');\n}\n

\n\n

注意点3:クラスインスタンスの判定

\n

クラスのインスタンスは typeof では ‘object’ になるため、instanceof を使う必要があります。

\n\n

class User {\n  constructor(public name: string) {}\n}\n\nconst user = new User('John');\n\n// typeof では区別できない\nconsole.log(typeof user); // 'object'\n\n// instanceof を使う\nif (user instanceof User) {\n  console.log(`ユーザー: ${user.name}`);\n}\n\n// 型ガード関数にすると便利\nfunction isUser(value: unknown): value is User {\n  return value instanceof User;\n}\n

\n\n

注意点4:トランスパイルされたコードでの動作

\n

TypeScriptをJavaScriptにトランスパイルする際、型情報は失われます。実行時の型チェックは必ず必要です。

\n\n

// TypeScriptのコード\nfunction processData(data: string | number): void {\n  if (typeof data === 'string') {\n    console.log(data.toUpperCase());\n  } else {\n    console.log(data.toFixed(2));\n  }\n}\n\n// トランスパイル後のJavaScript\n// 型情報がすべて削除される\nfunction processData(data) {\n  if (typeof data === 'string') {\n    console.log(data.toUpperCase());\n  } else {\n    console.log(data.toFixed(2));\n  }\n}\n\n// 実行時に型チェックが必要\nprocessData('hello'); // 正常\nprocessData(42);      // 正常\nprocessData(true);    // エラー!(booleanは想定していない)\n

\n\n

注意点5:パフォーマンスを意識した使用

\n

ループ内で何度も typeof チェックを行う場合、パフォーマンスに注意が必要です。

\n\n

// 非効率な例:ループ内で毎回型チェック\nfunction processItems(items: unknown[]): void {\n  for (const item of items) {\n    if (typeof item === 'string') {\n      console.log(item.toUpperCase());\n    } else if (typeof item === 'number') {\n      console.log(item * 2);\n    }\n  }\n}\n\n// 効率的な例:事前にフィルタリング\nfunction processItems(items: unknown[]): void {\n  const strings = items.filter((item): item is string => typeof item === 'string');\n  const numbers = items.filter((item): item is number => typeof item === 'number');\n  \n  strings.forEach(str => console.log(str.toUpperCase()));\n  numbers.forEach(num => console.log(num * 2));\n}\n

\n

\n\n

\n

実務で使える便利なユーティリティ集

\n\n

実務で頻繁に使用するユーティリティ関数をまとめました。プロジェクトの common/ フォルダに配置すると便利です。

\n\n


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