🎯

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

に公開

概要

APIレスポンスの形式が異なる場合の処理方法について、Dart 3.0の機能を使用した2つのアプローチを説明します:

  1. sealed classを使用したアプローチ
  2. enumを使用したアプローチ

sealed classを使用したアプローチ

こちらがサンプル

// APIレスポンスの異なる形式を表現するための例

// 1. sealed classを使用した例
sealed class ApiResponse {
  const ApiResponse();
}

// 成功レスポンス(データあり)
final class SuccessWithData extends ApiResponse {
  final Map<String, dynamic> data;
  const SuccessWithData(this.data);
}

// 成功レスポンス(メッセージのみ)
final class SuccessWithMessage extends ApiResponse {
  final String message;
  const SuccessWithMessage(this.message);
}

// エラーレスポンス
final class ErrorResponse extends ApiResponse {
  final String error;
  final int code;
  const ErrorResponse(this.error, this.code);
}

// 2. enumを使用した例
enum ResponseType {
  success,
  error,
  loading;

  // enumの拡張メソッド
  String get message {
    return switch (this) {
      ResponseType.success => '成功しました',
      ResponseType.error => 'エラーが発生しました',
      ResponseType.loading => '読み込み中...',
    };
  }
}

// APIレスポンスを処理する関数例
void handleApiResponse(ApiResponse response) {
  // sealed classを使用したパターンマッチング
  final result = switch (response) {
    SuccessWithData(data: final data) => 'データ: $data',
    SuccessWithMessage(message: final msg) => 'メッセージ: $msg',
    ErrorResponse(error: final err, code: final code) => 'エラー: $err (コード: $code)',
  };
  
  print(result);
}

void main() {
  print('=== sealed classの例 ===');
  
  // 異なる形式のAPIレスポンスをシミュレート
  final responses = [
    SuccessWithData({'id': 1, 'name': 'テスト'}),
    SuccessWithMessage('処理が完了しました'),
    ErrorResponse('不正なリクエスト', 400),
  ];
  
  // 各レスポンスを処理
  for (final response in responses) {
    handleApiResponse(response);
  }
  
  print('\n=== enumの例 ===');
  
  // enumを使用した状態管理
  for (final type in ResponseType.values) {
    print('${type.name}: ${type.message}');
  }
  
  // enumを使用した分岐処理
  final status = ResponseType.success;
  final statusMessage = switch (status) {
    ResponseType.success => '✅ 成功',
    ResponseType.error => '❌ エラー',
    ResponseType.loading => '⏳ 読み込み中',
  };
  
  print('\n現在のステータス: $statusMessage');
}

実行結果:

=== sealed classの例 ===
データ: {id: 1, name: テスト}
メッセージ: 処理が完了しました
エラー: 不正なリクエスト (コード: 400)

=== enumの例 ===
success: 成功しました
error: エラーが発生しました
loading: 読み込み中...

現在のステータス: ✅ 成功

Exited.

特徴

  • 異なる形式のデータを型安全に扱える
  • パターンマッチングで網羅性チェックが可能
  • データを持つことができる
  • 継承関係を表現できる

実装例

sealed class ApiResponse {
  const ApiResponse();
}

final class SuccessWithData extends ApiResponse {
  final Map<String, dynamic> data;
  const SuccessWithData(this.data);
}

final class SuccessWithMessage extends ApiResponse {
  final String message;
  const SuccessWithMessage(this.message);
}

final class ErrorResponse extends ApiResponse {
  final String error;
  final int code;
  const ErrorResponse(this.error, this.code);
}

パターンマッチングの使用

final result = switch (response) {
  SuccessWithData(data: final data) => 'データ: $data',
  SuccessWithMessage(message: final msg) => 'メッセージ: $msg',
  ErrorResponse(error: final err, code: final code) => 
      'エラー: $err (コード: $code)',
};

enumを使用したアプローチ

特徴

  • シンプルな状態管理に適している
  • 限られた値の集合を表現できる
  • メソッドや計算プロパティを持てる
  • データを持つことはできない(Dart 3.0現在)

実装例

enum ResponseType {
  success,
  error,
  loading;

  String get message {
    return switch (this) {
      ResponseType.success => '成功しました',
      ResponseType.error => 'エラーが発生しました',
      ResponseType.loading => '読み込み中...',
    };
  }
}

使い分け

sealed classを使う場合

  • 異なる形式のデータを持つ必要がある場合
  • 複雑な状態や振る舞いを表現する必要がある場合
  • 型の安全性を最大限確保したい場合

enumを使う場合

  • 単純な状態管理が目的の場合
  • 限られた値の集合を表現する場合
  • データを持つ必要がない場合

ベストプラクティス

sealed class

  1. 基底クラスはsealedで定義し、サブクラスはfinalで定義する
  2. constコンストラクタを使用してイミュータブルにする
  3. パターンマッチングで全てのケースを網羅する

enum

  1. 関連する定数の集合を表現する
  2. 拡張メソッドを活用して振る舞いを追加する
  3. switchステートメントで全てのケースを網羅する

エラーハンドリング例

try {
  handleApiResponse(response);
} on Exception catch (e) {
  print('エラーが発生しました: $e');
}

パフォーマンスの考慮事項

  • sealed class: インスタンス生成のオーバーヘッドあり
  • enum: 単純な値の比較のみなので高速

Dart 3.0の新機能活用

  • パターンマッチング
  • 網羅性チェック
  • recordとの組み合わせ
  • switchの式としての使用

Discussion