Tasuke Hubのロゴ

ITを中心に困っている人を助けるメディア

分かりやすく解決策を提供することで、あなたの困ったをサポート。 全ての人々がスムーズに生活できる世界を目指します。

TypeScript非同期パターン完全ガイド2025:最新のベストプラクティスと実装テクニック

記事のサムネイル
TH

Tasuke Hub管理人

東証プライム市場上場企業エンジニア

情報系修士卒業後、大手IT企業にてフルスタックエンジニアとして活躍。 Webアプリケーション開発からクラウドインフラ構築まで幅広い技術に精通し、 複数のプロジェクトでリードエンジニアを担当。 技術ブログやオープンソースへの貢献を通じて、日本のIT技術コミュニティに積極的に関わっている。

🎓情報系修士🏢東証プライム上場企業💻フルスタックエンジニア📝技術ブログ執筆者

TypeScript非同期パターン完全ガイド2025:最新のベストプラクティスと実装テクニック

TypeScriptにおける非同期処理の基礎と進化

TypeScriptでの非同期処理は、モダンなウェブアプリケーション開発において不可欠なスキルです。2025年現在、非同期処理のパターンは進化を続け、より効率的で保守性の高いコードを書くための新しいアプローチが登場しています。

非同期処理の歴史的変遷

JavaScriptとTypeScriptにおける非同期処理の歴史を振り返ると、その進化は明らかです:

  1. コールバック関数: 初期の非同期処理方法で、「コールバック地獄」と呼ばれる問題を引き起こしました
  2. Promise: ES2015で導入され、非同期処理の連鎖を読みやすく書けるようになりました
  3. async/await: ES2017で導入され、非同期コードを同期的に書けるようになりました
  4. 現代のパターン: 2025年では、これらの基本技術を組み合わせた高度なパターンが一般的になっています
// コールバック方式(古い方法)
function fetchData(callback: (data: any, error?: Error) => void): void {
  setTimeout(() => {
    try {
      const data = { name: "TypeScript", version: "5.4" };
      callback(data);
    } catch (error) {
      callback(null, error as Error);
    }
  }, 1000);
}

// Promise方式
function fetchDataPromise(): Promise<{ name: string; version: string }> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      try {
        const data = { name: "TypeScript", version: "5.4" };
        resolve(data);
      } catch (error) {
        reject(error);
      }
    }, 1000);
  });
}

// async/await方式(現代的)
async function fetchDataModern(): Promise<{ name: string; version: string }> {
  // 実際のアプリケーションではここでAPIリクエストなどを行う
  return { name: "TypeScript", version: "5.4" };
}

TypeScriptは型システムを通じて、これらの非同期パターンに安全性を提供します。特に、Promiseの戻り値の型を明示することで、非同期処理の結果の型を保証できます。

2025年のPromiseパターン:高度な使い方

Promiseは非同期処理の基盤として今も重要な役割を果たしています。2025年においては、より洗練された使い方が標準となっています。

Promise Combinatorsの効果的な活用

TypeScriptでは、複数のPromiseを組み合わせるための様々なメソッドが提供されています:

// 複数のAPIエンドポイントからデータを取得する例
async function fetchDashboardData(userId: string): Promise<DashboardData> {
  // 並列実行:すべてのPromiseが完了するまで待機
  const [user, settings, notifications] = await Promise.all([
    fetchUser(userId),
    fetchUserSettings(userId),
    fetchUserNotifications(userId)
  ]);
  
  return {
    user,
    settings,
    notifications,
    lastUpdated: new Date()
  };
}

// 最初に完了したPromiseの結果を使用
async function fetchFromFastestMirror<T>(urls: string[]): Promise<T> {
  const promises = urls.map(url => fetch(url).then(r => r.json()));
  return Promise.any(promises); // 最初に成功したPromiseの結果を返す
}

// すべてのPromiseの結果を取得(失敗したものも含む)
async function attemptAllOperations<T>(operations: Promise<T>[]): Promise<(T | Error)[]> {
  const results = await Promise.allSettled(operations);
  
  return results.map(result => {
    if (result.status === 'fulfilled') {
      return result.value;
    } else {
      return result.reason;
    }
  });
}

Promiseチェーンの最適化

長いPromiseチェーンは読みにくくなりがちですが、適切に構造化することで可読性を向上させることができます:

// 構造化されたPromiseチェーンの例
function processUserData(userId: string): Promise<ProcessedUserData> {
  return fetchUser(userId)
    .then(user => {
      // 中間結果をログに記録
      console.log(`User ${user.name} fetched`);
      
      // 次の処理に必要なデータだけを渡す
      return {
        userId: user.id,
        preferences: user.preferences
      };
    })
    .then(({ userId, preferences }) => {
      // 並列でデータを取得
      return Promise.all([
        fetchRecommendations(userId, preferences),
        fetchHistory(userId)
      ]);
    })
    .then(([recommendations, history]) => {
      // 最終的な処理結果を返す
      return {
        recommendations: recommendations.filter(r => r.score > 0.8),
        recentItems: history.items.slice(0, 5)
      };
    })
    .catch(error => {
      // エラーハンドリングと回復ロジック
      console.error(`Failed to process user data: ${error.message}`);
      return {
        recommendations: [],
        recentItems: []
      };
    });
}

Promiseのキャンセル処理

2025年のTypeScriptでは、AbortControllerを使用したPromiseのキャンセル処理が標準的になっています:

// キャンセル可能なAPI呼び出し
function fetchWithTimeout<T>(url: string, timeoutMs = 5000): Promise<T> {
  const controller = new AbortController();
  const { signal } = controller;
  
  // タイムアウト用のタイマー
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
  
  return fetch(url, { signal })
    .then(response => {
      clearTimeout(timeoutId);
      if (!response.ok) {
        throw new Error(`HTTP error: ${response.status}`);
      }
      return response.json() as Promise<T>;
    })
    .catch(error => {
      clearTimeout(timeoutId);
      if (error.name === 'AbortError') {
        throw new Error(`Request timed out after ${timeoutMs}ms`);
      }
      throw error;
    });
}

async/awaitの最適化パターン

async/await構文は非同期コードを同期的に書けるようにする強力な機能ですが、効果的に使用するにはいくつかのパターンを理解する必要があります。

エラーハンドリングの最適化

try/catchブロックを使用したエラーハンドリングは、async/await構文の大きな利点の一つです:

// 構造化されたエラーハンドリング
async function fetchUserProfile(userId: string): Promise<UserProfile | null> {
  try {
    const user = await fetchUser(userId);
    
    try {
      // ネストされたtry/catchで特定の操作のエラーを個別に処理
      const posts = await fetchUserPosts(user.id);
      return {
        ...user,
        recentPosts: posts.slice(0, 5)
      };
    } catch (postError) {
      // 投稿の取得に失敗しても、ユーザー情報は返す
      console.warn(`Failed to fetch posts: ${postError.message}`);
      return {
        ...user,
        recentPosts: []
      };
    }
  } catch (userError) {
    // ユーザー情報の取得に失敗した場合
    console.error(`Failed to fetch user: ${userError.message}`);
    return null;
  }
}

並列処理の最適化

async/await構文を使用する際、複数の非同期処理を効率的に並列実行することが重要です:

// 非効率な逐次実行
async function fetchDataSequential(ids: string[]): Promise<Data[]> {
  const results = [];
  
  // 一つずつ順番に実行(遅い)
  for (const id of ids) {
    const data = await fetchData(id);
    results.push(data);
  }
  
  return results;
}

// 効率的な並列実行
async function fetchDataParallel(ids: string[]): Promise<Data[]> {
  // すべてのPromiseを同時に開始し、結果を待つ(速い)
  const promises = ids.map(id => fetchData(id));
  return Promise.all(promises);
}

// 制御された並列実行(バッチ処理)
async function fetchDataInBatches(ids: string[], batchSize = 5): Promise<Data[]> {
  const results: Data[] = [];
  
  // IDsをバッチに分割
  for (let i = 0; i < ids.length; i += batchSize) {
    const batchIds = ids.slice(i, i + batchSize);
    
    // バッチ内のリクエストを並列実行
    const batchPromises = batchIds.map(id => fetchData(id));
    const batchResults = await Promise.all(batchPromises);
    
    results.push(...batchResults);
  }
  
  return results;
}

async/awaitとジェネレーターの組み合わせ

複雑な非同期フローを制御するために、async/awaitとジェネレーターを組み合わせることができます:

// ジェネレーターを使った段階的なデータ処理
async function* processLargeDataset<T, R>(
  items: T[],
  processor: (item: T) => Promise<R>,
  batchSize = 100
): AsyncGenerator<R[], void, unknown> {
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    
    // バッチを並列処理
    const promises = batch.map(item => processor(item));
    const results = await Promise.all(promises);
    
    // 各バッチの結果を生成
    yield results;
    
    // 進捗報告
    console.log(`Processed ${Math.min(i + batchSize, items.length)}/${items.length} items`);
  }
}

// 使用例
async function runDataProcessing() {
  const items = await fetchLargeDataset();
  const processor = processLargeDataset(items, processItem, 50);
  
  let processedCount = 0;
  
  // 各バッチの結果を処理
  for await (const batchResults of processor) {
    await saveBatchResults(batchResults);
    processedCount += batchResults.length;
    
    // 進捗を更新
    updateProgressUI(processedCount, items.length);
  }
  
  console.log('Processing complete!');
}

高度なエラーハンドリングパターン

非同期処理におけるエラーハンドリングは、堅牢なアプリケーションを構築する上で非常に重要です。2025年のTypeScriptでは、より洗練されたエラーハンドリングパターンが一般的になっています。

型安全なエラーハンドリング

TypeScriptの型システムを活用して、より安全なエラーハンドリングを実現できます:

// カスタムエラークラスの定義
class APIError extends Error {
  constructor(
    message: string,
    public statusCode: number,
    public errorCode: string
  ) {
    super(message);
    this.name = 'APIError';
    
    // Errorオブジェクトのプロトタイプチェーンを正しく設定
    Object.setPrototypeOf(this, APIError.prototype);
  }
  
  isClientError(): boolean {
    return this.statusCode >= 400 && this.statusCode < 500;
  }
  
  isServerError(): boolean {
    return this.statusCode >= 500;
  }
  
  // エラーに応じた適切なユーザーメッセージを返す
  getUserMessage(): string {
    if (this.statusCode === 401) {
      return 'セッションが切れました。再度ログインしてください。';
    } else if (this.statusCode === 403) {
      return 'この操作を行う権限がありません。';
    } else if (this.statusCode === 404) {
      return '要求されたリソースが見つかりませんでした。';
    } else if (this.isServerError()) {
      return 'サーバーエラーが発生しました。しばらく経ってからもう一度お試しください。';
    } else {
      return 'エラーが発生しました。';
    }
  }
}

// Result型パターンの実装
type Result<T, E = Error> = 
  | { success: true; value: T }
  | { success: false; error: E };

// Result型を返す関数
async function fetchUserSafely(userId: string): Promise<Result<User, APIError>> {
  try {
    const response = await fetch(`/api/users/${userId}`);
    
    if (!response.ok) {
      const errorData = await response.json().catch(() => ({}));
      return {
        success: false,
        error: new APIError(
          errorData.message || 'Failed to fetch user',
          response.status,
          errorData.code || 'UNKNOWN_ERROR'
        )
      };
    }
    
    const user = await response.json();
    return { success: true, value: user };
  } catch (error) {
    return {
      success: false,
      error: new APIError(
        error instanceof Error ? error.message : 'Unknown error',
        500,
        'NETWORK_ERROR'
      )
    };
  }
}

// 使用例
async function displayUserProfile(userId: string): Promise<void> {
  const result = await fetchUserSafely(userId);
  
  if (result.success) {
    // 型安全:TypeScriptはresult.valueがUser型であることを認識
    renderUserProfile(result.value);
  } else {
    // 型安全:TypeScriptはresult.errorがAPIError型であることを認識
    showErrorMessage(result.error.getUserMessage());
    
    // エラーの種類に応じた処理
    if (result.error.statusCode === 401) {
      redirectToLogin();
    } else if (result.error.isServerError()) {
      reportErrorToMonitoring(result.error);
    }
  }
}

リトライメカニズム

一時的なエラーに対処するためのリトライメカニズムも重要です:

// 指数バックオフを使用したリトライ関数
async function withRetry<T>(
  operation: () => Promise<T>,
  options: {
    maxRetries?: number;
    initialDelay?: number;
    maxDelay?: number;
    backoffFactor?: number;
    retryableErrors?: (error: unknown) => boolean;
  } = {}
): Promise<T> {
  const {
    maxRetries = 3,
    initialDelay = 300,
    maxDelay = 10000,
    backoffFactor = 2,
    retryableErrors = (error) => true
  } = options;
  
  let lastError: unknown;
  let delay = initialDelay;
  
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error) {
      lastError = error;
      
      // 最後の試行でエラーが発生した場合、またはリトライ可能なエラーでない場合はエラーをスロー
      if (attempt === maxRetries || !retryableErrors(error)) {
        throw error;
      }
      
      console.log(`Attempt ${attempt + 1}/${maxRetries} failed. Retrying in ${delay}ms...`);
      
      // 指定された時間だけ待機
      await new Promise(resolve => setTimeout(resolve, delay));
      
      // 次回の待機時間を計算(指数バックオフ)
      delay = Math.min(delay * backoffFactor, maxDelay);
    }
  }
  
  // コンパイラのためのフォールバック(実際には到達しない)
  throw lastError;
}

まとめ

2025年のTypeScriptにおける非同期パターンは、単なる基本的な非同期処理から、より洗練された高度なパターンへと進化しています。この記事で紹介した主なポイントは以下の通りです:

  1. Promiseの高度な使い方:Promise Combinatorsの活用、チェーンの最適化、キャンセル処理
  2. async/awaitの最適化:効率的なエラーハンドリング、並列処理の最適化、ジェネレーターとの組み合わせ
  3. 高度なエラーハンドリング:型安全なエラーハンドリング、Result型パターン、リトライメカニズム

これらのパターンを適切に組み合わせることで、より保守性が高く、パフォーマンスの良い非同期コードを書くことができます。TypeScriptの型システムを最大限に活用することで、コンパイル時にエラーを検出し、より堅牢なアプリケーションを構築することができるでしょう。

非同期処理は常に進化しており、新しいパターンやベストプラクティスが登場し続けています。最新の動向に注目しながら、自分のプロジェクトに最適なパターンを選択していくことが重要です。

このトピックはこちらの書籍で勉強するのがおすすめ!

この記事の内容をさらに深く理解したい方におすすめの一冊です。実践的な知識を身につけたい方は、ぜひチェックしてみてください!

おすすめ記事

おすすめコンテンツ