TypeScript never型を実務で使いこなす!型安全性を極める実装パターン

TypeScript

TypeScript never型を実務で使いこなす!型安全性を極める実装パターン

1. never型とは?簡易的な解説

TypeScriptのnever型は、「値が絶対に存在しない」ことを表す特殊な型です。関数が値を返さない(例外をスローするか無限ループ)場合や、ユニオン型の絞り込みで論理的に不可能な分岐を検出する際に使われます。

簡単に言えば、neverは「ここに到達するはずがない」という強い宣言であり、これを活用することでコンパイル時に多くのバグを防ぐことができます。

2. 業務でよく遭遇するnever型のユースケース

2.1 網羅性チェック(Exhaustiveness Check)

最も実務的で重要なユースケースが、switch文やif-else分岐の網羅性チェックです。ステータス管理やAPIのレスポンス処理など、有限の状態を扱う場面で非常に有効です。

例えば、注文ステータスを管理するシステムを想像してください。新しいステータスを追加したとき、すべての処理箇所で対応する必要があります。neverを使うことで、対応漏れを自動的に検出できます。

2.2 エラーハンドリングの厳格化

APIからのエラーレスポンスやバリデーションエラーを扱う際、すべてのエラーケースを網羅したかを型レベルで確認できます。

2.3 再帰的な型安全性

複雑なデータ構造を扱う場合、neverを使って不正な構造の生成を防ぐことができます。

3. 実務コード例集

3.1 ステータス管理での網羅性チェック

実際のプロジェクトで使える、注文管理システムの例です。

// ステータスの定義
type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled';

interface Order {
  id: string;
  status: OrderStatus;
  amount: number;
}

// ステータスごとの処理を行う関数
function handleOrderStatus(order: Order): string {
  switch (order.status) {
    case 'pending':
      return '決済処理を開始します';
    case 'processing':
      return '商品を梱包中です';
    case 'shipped':
      return '配送中です';
    case 'delivered':
      return '配送完了しました';
    case 'cancelled':
      return '注文はキャンセルされました';
    default:
      const _exhaustiveCheck: never = order.status;
      return _exhaustiveCheck;
  }
}

// 新しいステータスを追加した場合
type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled' | 'returned';

// handleOrderStatus関数でコンパイルエラーが発生
// "Type 'returned' is not assignable to type 'never'" エラーが出て、
// 'returned'ケースの処理を追加する必要があることを教えてくれます

3.2 API レスポンスの型安全な処理

実際のAPI連携で使う、エラーハンドリングの例です。

// APIレスポンスの型定義
type ApiResponse<T> = 
  | { success: true; data: T; error: null }
  | { success: false; data: null; error: 'NETWORK_ERROR' }
  | { success: false; data: null; error: 'VALIDATION_ERROR' }
  | { success: false; data: null; error: 'UNAUTHORIZED' }
  | { success: false; data: null; error: 'SERVER_ERROR' };

interface UserData {
  id: number;
  name: string;
  email: string;
}

function processUserResponse(response: ApiResponse<UserData>): void {
  if (response.success) {
    console.log(`ユーザー: ${response.data.name}`);
    return;
  }

  // エラーハンドリング
  switch (response.error) {
    case 'NETWORK_ERROR':
      console.error('ネットワークエラーが発生しました');
      break;
    case 'VALIDATION_ERROR':
      console.error('入力値が不正です');
      break;
    case 'UNAUTHORIZED':
      console.error('認証が必要です');
      break;
    case 'SERVER_ERROR':
      console.error('サーバーエラーが発生しました');
      break;
    default:
      const _exhaustiveCheck: never = response.error;
      return _exhaustiveCheck;
  }
}

// 新しいエラータイプを追加したら、switch文でコンパイルエラーになる

3.3 フォームバリデーションエラーの厳格な管理

実務でよく使うフォーム検証のパターンです。

type ValidationErrorType = 
  | 'REQUIRED'
  | 'EMAIL_INVALID'
  | 'MIN_LENGTH'
  | 'MAX_LENGTH'
  | 'PASSWORD_WEAK';

interface ValidationError {
  field: string;
  type: ValidationErrorType;
  message: string;
}

function getErrorMessage(error: ValidationError): string {
  switch (error.type) {
    case 'REQUIRED':
      return `${error.field}は必須項目です`;
    case 'EMAIL_INVALID':
      return '有効なメールアドレスを入力してください';
    case 'MIN_LENGTH':
      return `${error.field}は${error.message}文字以上である必要があります`;
    case 'MAX_LENGTH':
      return `${error.field}は${error.message}文字以下である必要があります`;
    case 'PASSWORD_WEAK':
      return 'パスワードは大文字、小文字、数字を含む必要があります';
    default:
      const _exhaustiveCheck: never = error.type;
      return _exhaustiveCheck;
  }
}

3.4 複雑な条件分岐の型安全性確保

複数の条件を組み合わせた処理で、すべてのパターンが処理されていることを保証する例です。

type UserRole = 'admin' | 'manager' | 'user';
type Environment = 'production' | 'staging' | 'development';

interface PermissionContext {
  role: UserRole;
  env: Environment;
}

function getAccessLevel(context: PermissionContext): number {
  if (context.role === 'admin') {
    return 100;
  }
  
  if (context.role === 'manager') {
    if (context.env === 'production') {
      return 50;
    } else if (context.env === 'staging' || context.env === 'development') {
      return 80;
    }
  }
  
  if (context.role === 'user') {
    if (context.env === 'production') {
      return 10;
    } else if (context.env === 'staging' || context.env === 'development') {
      return 30;
    }
  }
  
  // すべてのケースが処理されていることを保証
  const _exhaustiveCheck: never = context as never;
  return _exhaustiveCheck;
}

3.5 ライブラリやプラグインの互換性チェック

複数のプラグイン実装を強制する場面で使える例です。

type LoggerType = 'console' | 'file' | 'cloud';

interface LoggerPlugin {
  type: LoggerType;
  log(message: string): void;
}

class ConsoleLogger implements LoggerPlugin {
  type: 'console' = 'console';
  log(message: string): void {
    console.log(message);
  }
}

class FileLogger implements LoggerPlugin {
  type: 'file' = 'file';
  log(message: string): void {
    // ファイルに書き込む処理
  }
}

class CloudLogger implements LoggerPlugin {
  type: 'cloud' = 'cloud';
  log(message: string): void {
    // クラウドに送信する処理
  }
}

function createLogger(type: LoggerType): LoggerPlugin {
  switch (type) {
    case 'console':
      return new ConsoleLogger();
    case 'file':
      return new FileLogger();
    case 'cloud':
      return new CloudLogger();
    default:
      const _exhaustiveCheck: never = type;
      return _exhaustiveCheck;
  }
}

// LoggerTypeに新しい型を追加すると、createLogger関数でエラーが出る

4. よくある応用パターン

4.1 ジェネリクスとneverの組み合わせ

より高度な型安全性が必要な場合、ジェネリクスとneverを組み合わせます。

// T型がユニオン型に含まれていることを保証する
type Extends<T, U> = T extends U ? true : never;

// 使用例
type CheckOrderStatus = Extends<'pending' | 'shipped', OrderStatus>; // true
type CheckInvalid = Extends<'invalid', OrderStatus>; // never(コンパイルエラー)

// 実務例:許可されたロールのみを受け付ける関数
function checkRolePermission<T extends UserRole>(role: T): void {
  if ((role as string) === 'superadmin') {
    // 'superadmin'はUserRoleに含まれないため、ここには到達できない
    const _check: never = role;
  }
}

4.2 分別可能なユニオン型の完全処理

discriminated unionパターンでの厳格なチェックです。

type Result<T, E> = 
  | { type: 'success'; value: T }
  | { type: 'error'; error: E };

function processResult<T, E>(result: Result<T, E>): T | null {
  switch (result.type) {
    case 'success':
      return result.value;
    case 'error':
      console.error(result.error);
      return null;
    default:
      const _exhaustiveCheck: never = result;
      return _exhaustiveCheck;
  }
}

4.3 状態機械の遷移検証

有限状態機械(FSM)の遷移をneverで検証する実装です。

type PaymentState = 'idle' | 'processing' | 'completed' | 'failed';

interface StateTransition {
  from: PaymentState;
  to: PaymentState;
}

function isValidTransition(transition: StateTransition): boolean {
  // idleから遷移可能な状態
  if (transition.from === 'idle') {
    return transition.to === 'processing';
  }
  
  // processingから遷移可能な状態
  if (transition.from === 'processing') {
    return transition.to === 'completed' || transition.to === 'failed';
  }
  
  // completedとfailedからは遷移不可
  if (transition.from === 'completed' || transition.from === 'failed') {
    return false;
  }
  
  // すべてのケースを処理
  const _exhaustiveCheck: never = transition.from;
  return _exhaustiveCheck;
}

5. 注意点と落とし穴

5.1 any型との混在

neverの効果を無効にしないように注意が必要です。

// ❌ 避けるべき:anyを使うとneverチェックが機能しない
function badHandler(status: OrderStatus): string {
  const anyStatus: any = status;
  switch (anyStatus) {
    case 'pending':
      return 'pending';
    // 他のケースがなくてもエラーにならない
    default:
      return 'unknown';
  }
}

// ✅ 推奨:neverを使う
function goodHandler(status: OrderStatus): string {
  switch (status) {
    case 'pending':
      return 'pending';
    case 'processing':
      return 'processing';
    case 'shipped':
      return 'shipped';
    case 'delivered':
      return 'delivered';
    case 'cancelled':
      return 'cancelled';
    default:
      const _exhaustiveCheck: never = status;
      return _exhaustiveCheck;
  }
}

5.2 型アサーション時の危険性

// ❌ 危険:型アサーションでneverチェックを回避できてしまう
const invalidStatus = 'invalid' as OrderStatus;
const _check: never = invalidStatus; // これでもエラーが出ない(悪い実装)

// ✅ 安全:アサーションを使わない設計
function createOrderStatus(status: string): OrderStatus {
  if (status === 'pending' || status === 'processing' || 
      status === 'shipped' || status === 'delivered' || 
      status === 'cancelled') {
    return status as OrderStatus;
  }
  throw new Error(`Invalid status: ${status}`);
}

5.3 複雑さとの バランス

すべての分岐でneverチェックを使う必要はありません。以下の場合は過度な設計になる可能性があります。

  • 頻繁に新しい値が追加されるユニオン型
  • 外部APIから動的に値が来る場合(この場合はバリデーションの方が適切)
  • 単純な条件分岐(3値以下など)

5.4 エラーメッセージの曖昧さ

// neverへの代入時、エラーメッセージが分かりづらい場合がある
// より明確なエラーを出すヘルパー関数を使用
function assertNever(value: never, message?: string): never {
  throw new Error(
    message || `Unexpected value: ${value}. All cases should have been handled.`
  );
}

function handleStatus(status: OrderStatus): string {
  switch (status) {
    case 'pending':
      return 'pending';
    // 他のケースが漏れている
    default:
      return assertNever(
        status,
        `OrderStatusの未対応ケース: ${status}`
      );
  }
}

6. 実務での推奨プラクティス

6.1 ステータス/ステート系は必ずneverで保護

アプリケーションの状態を管理する部分は、neverを使った網羅性チェックを実装しましょう。これにより、新機能追加時のバグを防ぐことができます。

6.2 外部連携のレスポンス処理には必須

API、データベース、メッセージキューなどからのレスポンスを処理する際は、すべてのケースをカバーしていることをneverで保証します。

6.3 チーム開発では明示的なコメントを付ける

default:
  // このコードに到達することは論理上不可能
  // 新しい状態が追加された場合、必ずこのswitch文を修正する必要があります
  const _exhaustiveCheck: never = status;
  return _exhaustiveCheck;

まとめ

TypeScriptのnever型は、単なる「値が返されない関数」を表す型ではなく、実務では**型安全性を保証する強力なツール**です。特に以下の場面で非常に有効です。

  • ステータス・状態管理:すべての状態が処理されていることを保証
  • エラーハンドリング:すべてのエラーケースを漏れなく処理
  • 機能拡張:新しい値を追加したときに、関連する処理を自動的に検出
  • チーム開発:コードレビュー時の見落としを防ぐ

neverを使うことで、実装時のうっかり漏れをコンパイル時に発見でき、本番環境でのバグを大幅に削減できます。特に複雑なビジネスロジックを扱うプロジェクトでは、積極的に活用することをお勧めします。

ただし、すべての分岐に無理やり適用するのではなく、実装の複雑さとメリットのバランスを考慮して、適切に取り入れることが重要です。

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