Tasuke Hubのロゴ

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

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

TypeScript非同期処理パターン完全ガイド:エラーハンドリングから並行処理まで

記事のサムネイル
TH

Tasuke Hub管理人

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

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

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

TypeScript非同期処理パターン完全ガイド:エラーハンドリングから並行処理まで

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

TypeScriptでの非同期処理は、ウェブアプリケーション開発において避けて通れない重要な概念です。APIリクエスト、ファイル操作、データベースアクセスなど、多くの操作が非同期的に実行されます。

コールバック関数から始まる非同期処理

JavaScriptの初期の非同期処理はコールバック関数を使って実装されていました。

// コールバックを使った非同期処理の例
function fetchData(callback: (data: any, error?: Error) => void): void {
  setTimeout(() => {
    try {
      const data = { name: "TypeScript", version: "5.0" };
      callback(data);
    } catch (error) {
      callback(null, error as Error);
    }
  }, 1000);
}

// 使用例
fetchData((data, error) => {
  if (error) {
    console.error("エラーが発生しました:", error);
    return;
  }
  console.log("データを取得しました:", data);
});

しかし、このアプローチでは複数の非同期処理を連結するとコールバック地獄(Callback Hell)と呼ばれる読みにくく保守しづらいコードになってしまいます。

Promiseによる解決

ES2015でPromiseが導入され、非同期処理の書き方が改善されました。TypeScriptは型安全性を持ってPromiseをサポートしています。

// Promiseを使った非同期処理の例
function fetchData(): Promise<{ name: string; version: string }> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      try {
        const data = { name: "TypeScript", version: "5.0" };
        resolve(data);
      } catch (error) {
        reject(error);
      }
    }, 1000);
  });
}

// 使用例
fetchData()
  .then(data => {
    console.log("データを取得しました:", data);
  })
  .catch(error => {
    console.error("エラーが発生しました:", error);
  });

Promiseの主な状態は以下の3つです:

  1. Pending(保留中): 初期状態、非同期処理の結果はまだ利用できない
  2. Fulfilled(成功): 非同期処理が正常に完了し、結果が利用可能
  3. Rejected(失敗): 非同期処理がエラーで失敗

TypeScriptでは、Promiseの返り値の型を明示することで、非同期処理の結果の型を保証できます。これにより、IDEの自動補完や型チェックの恩恵を受けることができます。

Promiseを使った効率的な非同期処理の実装

Promiseは非同期処理を効率的に実装するための強力なツールです。TypeScriptと組み合わせることでさらに堅牢なコードを書くことができます。

Promiseチェーンの構築

Promiseの大きな利点は、.then()メソッドを使って処理を連鎖させられることです。これにより、複数の非同期処理を順番に実行する「Promiseチェーン」を構築できます。

// Promiseチェーンの例
function fetchUserData(userId: string): Promise<User> {
  return fetch(`https://api.example.com/users/${userId}`)
    .then(response => {
      if (!response.ok) {
        throw new Error(`APIエラー: ${response.status}`);
      }
      return response.json();
    })
    .then(data => {
      return {
        id: data.id,
        name: data.name,
        email: data.email
      } as User;
    });
}

// 使用例
fetchUserData("123")
  .then(user => {
    console.log("ユーザーデータを取得しました:", user);
    return fetchUserPosts(user.id);
  })
  .then(posts => {
    console.log("ユーザーの投稿を取得しました:", posts);
  })
  .catch(error => {
    console.error("エラーが発生しました:", error);
  });

.then()の戻り値は新しいPromiseとなるため、非同期処理を連鎖させることができます。

Promise.allを使った並列処理

複数の非同期処理を並行して実行し、すべてが完了したら次の処理に進みたい場合はPromise.all()が便利です。

// Promise.allの例
async function fetchAllUsersData(userIds: string[]): Promise<User[]> {
  const promises = userIds.map(id => fetchUserData(id));
  return Promise.all(promises);
}

// 使用例
fetchAllUsersData(["123", "456", "789"])
  .then(users => {
    console.log("すべてのユーザーデータを取得しました:", users);
  })
  .catch(error => {
    console.error("いずれかのリクエストでエラーが発生しました:", error);
  });

Promise.all()は、渡されたすべてのPromiseが解決されるとそれらの結果を配列で返します。いずれかのPromiseが失敗すると、すぐにエラーをスローします。

Promise.raceを使ったタイムアウト実装

リクエストに時間制限を設けたい場合、Promise.race()を使ってタイムアウト処理を実装できます。

// タイムアウト関数の定義
function timeout(ms: number): Promise<never> {
  return new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error(`操作がタイムアウトしました (${ms}ms)`));
    }, ms);
  });
}

// Promise.raceを使ったタイムアウト処理の例
function fetchWithTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
  return Promise.race([
    promise,
    timeout(ms)
  ]);
}

// 使用例
fetchWithTimeout(fetchUserData("123"), 5000)
  .then(user => {
    console.log("ユーザーデータを取得しました:", user);
  })
  .catch(error => {
    console.error("エラーが発生しました:", error);
  });

Promise.race()は、渡されたPromiseのうち最初に解決または拒否されたものの結果を返します。この特性を利用して、指定した時間内に処理が完了しない場合にタイムアウトさせることができます。

TypeScriptのジェネリック型を活用することで、どのような型の非同期処理でも扱えるようになります。

async/awaitを活用したコードの可読性向上

ES2017で導入されたasync/await構文は、Promiseベースの非同期コードをさらに読みやすく直感的に書けるようにしました。TypeScriptは完全にこの構文をサポートしています。

async/awaitの基本

async/awaitは、非同期処理を同期処理のように書けるようにする構文糖です。async関数内でawaitキーワードを使用すると、Promiseの解決を待機することができます。

// async/awaitを使用した例
async function fetchUserData(userId: string): Promise<User> {
  const response = await fetch(`https://api.example.com/users/${userId}`);
  
  if (!response.ok) {
    throw new Error(`APIエラー: ${response.status}`);
  }
  
  const data = await response.json();
  
  return {
    id: data.id,
    name: data.name,
    email: data.email
  } as User;
}

// 使用例
async function displayUserInfo(userId: string): Promise<void> {
  try {
    const user = await fetchUserData(userId);
    console.log("ユーザーデータを取得しました:", user);
    
    const posts = await fetchUserPosts(user.id);
    console.log("ユーザーの投稿を取得しました:", posts);
  } catch (error) {
    console.error("エラーが発生しました:", error);
  }
}

// 関数の実行
displayUserInfo("123").then(() => {
  console.log("処理が完了しました");
});

asyncが付いた関数は常にPromiseを返します。関数内でreturnされた値は、そのPromiseの解決値になります。

TypeScriptでのasync/awaitの型付け

TypeScriptでは、async関数の戻り値の型は自動的にPromise<T>になります。関数が値Tを返す場合、実際の型はPromise<T>になります。

// 戻り値の型アノテーション
async function fetchData(): Promise<string> {
  // 文字列を返しているように見えますが、実際はPromise<string>を返します
  return "データ";
}

// これは以下と同等です
function fetchDataPromise(): Promise<string> {
  return Promise.resolve("データ");
}

try/catchによるエラーハンドリング

async/awaitを使用すると、通常の同期コードと同じようにtry/catchブロックでエラーを捕捉できます。

async function safelyFetchData(userId: string): Promise<User | null> {
  try {
    const user = await fetchUserData(userId);
    return user;
  } catch (error) {
    console.error("ユーザーデータの取得に失敗しました:", error);
    // エラーログを送信するなどの処理
    return null;
  }
}

この方法はPromiseの.catch()メソッドを使うよりも読みやすく、複数の非同期処理を含むコードでも一箇所でエラーハンドリングできます。

連続した非同期処理

複数の非同期処理を順番に実行する場合、async/awaitを使うとコードが非常に読みやすくなります。

async function processUserData(userId: string): Promise<ProcessedData> {
  // 順番に実行される非同期処理
  const user = await fetchUserData(userId);
  const posts = await fetchUserPosts(user.id);
  const comments = await fetchPostComments(posts[0].id);
  
  return {
    user,
    recentPost: posts[0],
    recentComments: comments
  };
}

Promiseチェーンで書く場合よりもインデントが少なく、流れが理解しやすくなります。

並列処理との組み合わせ

非同期処理を並列に実行しつつasync/awaitの読みやすさを維持するには、Promise.all()などと組み合わせます。

async function fetchDashboardData(userId: string): Promise<Dashboard> {
  // 並列に実行される非同期処理
  const [user, settings, notifications] = await Promise.all([
    fetchUserData(userId),
    fetchUserSettings(userId),
    fetchUserNotifications(userId)
  ]);
  
  return {
    user,
    settings,
    notificationCount: notifications.length
  };
}

これにより、3つのAPI呼び出しが同時に実行され、すべてが完了してから次の処理に進みます。

実践的なエラーハンドリングパターン

非同期処理においてエラーを適切に処理することは、堅牢なアプリケーションを構築する上で非常に重要です。TypeScriptの型システムを活用することで、より安全なエラーハンドリングが可能になります。

カスタムエラークラスの定義

TypeScriptでは、独自のエラークラスを定義することで、より詳細なエラー情報を扱えます。

// カスタムエラークラスの定義
class APIError extends Error {
  statusCode: number;
  
  constructor(message: string, statusCode: number) {
    super(message);
    this.name = 'APIError';
    this.statusCode = statusCode;
    
    // Errorオブジェクトのプロトタイプチェーンを正しく設定するために必要
    Object.setPrototypeOf(this, APIError.prototype);
  }
  
  isClientError(): boolean {
    return this.statusCode >= 400 && this.statusCode < 500;
  }
  
  isServerError(): boolean {
    return this.statusCode >= 500;
  }
}

// 使用例
async function fetchUserData(userId: string): Promise<User> {
  const response = await fetch(`https://api.example.com/users/${userId}`);
  
  if (!response.ok) {
    throw new APIError(`ユーザーデータの取得に失敗しました`, response.status);
  }
  
  return await response.json();
}

カスタムエラークラスを使用すると、エラーの種類ごとに異なる処理を実装できます。

try-catchとインスタンスチェック

TypeScriptでは、instanceof演算子を使って捕捉したエラーの種類を確認できます。

async function handleUserRequest(userId: string): Promise<void> {
  try {
    const user = await fetchUserData(userId);
    console.log("ユーザーデータ:", user);
  } catch (error) {
    if (error instanceof APIError) {
      if (error.statusCode === 404) {
        console.error(`ユーザーID ${userId} は存在しません`);
      } else if (error.isServerError()) {
        console.error(`サーバーエラー: ${error.message}`);
        // サーバーエラーを監視サービスに報告するなど
        reportToMonitoringService(error);
      }
    } else if (error instanceof TypeError) {
      console.error(`型エラー: ${error.message}`);
    } else {
      console.error(`予期しないエラー: ${error}`);
    }
  }
}

このアプローチにより、エラーの種類に応じて適切な処理を行えます。

Result型パターン

エラーを例外としてではなく、戻り値の一部として扱う「Result型パターン」も効果的です。

// Result型の定義
type Result<T, E = Error> = {
  success: true;
  value: T;
} | {
  success: false;
  error: E;
};

// Result型を返す関数
async function safelyFetchUserData(userId: string): Promise<Result<User, APIError>> {
  try {
    const response = await fetch(`https://api.example.com/users/${userId}`);
    
    if (!response.ok) {
      return {
        success: false,
        error: new APIError(`ユーザーデータの取得に失敗しました`, response.status)
      };
    }
    
    const user = await response.json();
    return {
      success: true,
      value: user
    };
  } catch (error) {
    return {
      success: false,
      error: error instanceof Error 
        ? new APIError(error.message, 500)
        : new APIError('不明なエラー', 500)
    };
  }
}

// 使用例
async function displayUserInfo(userId: string): Promise<void> {
  const result = await safelyFetchUserData(userId);
  
  if (result.success) {
    // TypeScriptは自動的にresult.valueがUser型であることを認識します
    console.log("ユーザー名:", result.value.name);
  } else {
    // エラーハンドリング
    if (result.error.statusCode === 404) {
      console.error("ユーザーが見つかりません");
    } else {
      console.error("エラー:", result.error.message);
    }
  }
}

Result型パターンを使用すると、エラーハンドリングがより明示的になり、開発者は返り値の型を見るだけでエラーの可能性を考慮する必要があることがわかります。

非同期関数内のエラー伝播

async/awaitを使用する場合、エラーはPromiseチェーンと同様に伝播します。

async function processUserWorkflow(userId: string): Promise<void> {
  try {
    // エラーが発生すると、そのエラーはこのtry/catchブロックでキャッチされます
    const user = await fetchUserData(userId);
    const posts = await fetchUserPosts(user.id);
    const comments = await fetchPostComments(posts[0].id);
    
    console.log("処理成功:", {
      user,
      postCount: posts.length,
      commentCount: comments.length
    });
  } catch (error) {
    console.error("ワークフロー処理中にエラーが発生しました:", error);
    // エラーを上位の呼び出し元に再スローすることもできます
    throw error;
  }
}

この方法では、一連の非同期操作中のいずれかでエラーが発生した場合でも、すべて単一のcatchブロックで処理できます。

複数の非同期処理を効率的に管理する方法

実務上、複数の非同期処理を効率的に管理することは頻繁に求められる課題です。TypeScriptを使って複数の非同期処理を整理し、管理する方法を見ていきましょう。

依存関係に基づく処理の整理

非同期処理の依存関係を視覚的に理解するためには、処理のフローを図式化すると効果的です。

// 依存関係に基づく処理の整理例
async function loadApplicationData(userId: string): Promise<AppData> {
  // 1. ユーザーデータのロード(他に依存しない)
  const userPromise = fetchUserData(userId);
  
  // 2. ユーザーの設定とシステム設定を並行でロード(他に依存しない)
  const settingsPromise = fetchUserSettings(userId);
  const systemConfigPromise = fetchSystemConfig();
  
  // これらの処理を並行して実行
  const [user, settings, systemConfig] = await Promise.all([
    userPromise, settingsPromise, systemConfigPromise
  ]);
  
  // 3. ユーザーとその設定に依存する処理
  const [permissions, favorites] = await Promise.all([
    fetchUserPermissions(user.role, settings.permissionLevel),
    fetchUserFavorites(user.id, settings.favoriteCount)
  ]);
  
  // 4. 最終的なデータの構築
  return {
    user,
    settings,
    permissions,
    favorites,
    systemConfig
  };
}

このようにコードを整理することで、依存関係の理解が容易になり、非同期処理の実行効率も向上します。

非同期処理のキャンセル

TypeScriptとAbortControllerを組み合わせることで、非同期処理をキャンセルできます。

// キャンセル可能な非同期処理の例
async function fetchWithTimeout<T>(
  url: string,
  options: RequestInit = {},
  timeoutMs: number = 5000
): Promise<T> {
  // AbortControllerの作成
  const controller = new AbortController();
  const { signal } = controller;
  
  // タイムアウトのセットアップ
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
  
  try {
    // シグナルをfetchオプションに追加
    const response = await fetch(url, { ...options, signal });
    
    if (!response.ok) {
      throw new Error(`Network response was not ok: ${response.status}`);
    }
    
    // レスポンスをJSON形式でパース
    return await response.json() as T;
  } catch (error) {
    // AbortError(タイムアウトによるキャンセル)の場合
    if (error instanceof Error && error.name === 'AbortError') {
      throw new Error(`Request timed out after ${timeoutMs}ms`);
    }
    
    // その他のエラー
    throw error;
  } finally {
    // タイムアウトタイマーのクリーンアップ
    clearTimeout(timeoutId);
  }
}

// 使用例
async function loadData(shouldCancel: boolean) {
  const controller = new AbortController();
  
  try {
    // 非同期処理の開始
    const dataPromise = fetch('/api/data', { signal: controller.signal });
    
    if (shouldCancel) {
      // ある条件下で処理をキャンセル
      controller.abort();
    }
    
    const response = await dataPromise;
    const data = await response.json();
    
    return data;
  } catch (error) {
    if (error instanceof Error && error.name === 'AbortError') {
      console.log('Fetch was cancelled');
    } else {
      console.error('Error fetching data:', error);
    }
  }
}

キューを使った連続的な非同期処理

同時実行数を制限しながら多数の非同期処理を実行する場合、キューを使うアプローチが効果的です。

// 非同期タスクキューの実装
class AsyncTaskQueue {
  private queue: (() => Promise<unknown>)[] = [];
  private concurrentLimit: number;
  private runningCount = 0;
  private isProcessing = false;
  
  constructor(concurrentLimit = 3) {
    this.concurrentLimit = concurrentLimit;
  }
  
  // タスクをキューに追加
  enqueue<T>(task: () => Promise<T>): Promise<T> {
    return new Promise<T>((resolve, reject) => {
      // タスクをラップしてキューに追加
      this.queue.push(async () => {
        try {
          const result = await task();
          resolve(result);
          return result;
        } catch (error) {
          reject(error);
          throw error;
        }
      });
      
      // キュー処理がまだ開始されていなければ開始
      if (!this.isProcessing) {
        this.processQueue();
      }
    });
  }
  
  // キューの処理
  private async processQueue(): Promise<void> {
    this.isProcessing = true;
    
    while (this.queue.length > 0 && this.runningCount < this.concurrentLimit) {
      const task = this.queue.shift();
      if (!task) continue;
      
      this.runningCount++;
      
      // タスクを実行し、完了後にrunningCountを減らす
      task().finally(() => {
        this.runningCount--;
        this.processQueue();
      });
    }
    
    this.isProcessing = this.runningCount > 0 || this.queue.length > 0;
  }
  
  // 現在のキューの状態を確認
  get status(): { queued: number; running: number } {
    return {
      queued: this.queue.length,
      running: this.runningCount
    };
  }
}

// 使用例
async function processLargeDataSet(items: string[]): Promise<Record<string, any>> {
  const taskQueue = new AsyncTaskQueue(5); // 最大5つの並行タスク
  const results: Record<string, any> = {};
  
  const tasks = items.map(item => 
    taskQueue.enqueue(async () => {
      const data = await fetchDataForItem(item);
      results[item] = data;
    })
  );
  
  // すべてのタスクが完了するまで待機
  await Promise.all(tasks);
  
  return results;
}

このアプローチは、APIレート制限などがある場合に特に効果的です。

リトライメカニズムの実装

不安定なネットワーク環境や一時的なサーバーエラーに対処するため、リトライ機能を実装することも重要です。

// 指数バックオフを使ったリトライ関数
async function withRetry<T>(
  fn: () => Promise<T>,
  options: {
    maxRetries?: number;
    initialDelay?: number;
    maxDelay?: number;
    backoffFactor?: number;
    retryableErrors?: (error: unknown) => boolean;
  } = {}
): Promise<T> {
  const {
    maxRetries = 3,
    initialDelay = 1000,
    maxDelay = 30000,
    backoffFactor = 2,
    retryableErrors = (error) => true // デフォルトではすべてのエラーをリトライ
  } = options;
  
  let lastError: unknown;
  let delay = initialDelay;
  
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } 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;
}

// 使用例
async function fetchDataWithRetry(url: string): Promise<any> {
  return withRetry(
    () => fetch(url).then(res => {
      if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
      return res.json();
    }),
    {
      maxRetries: 3,
      retryableErrors: (error) => {
        // ネットワークエラーや500系エラーのみリトライ
        return error instanceof Error && (
          error.message.includes('network') || 
          error.message.includes('HTTP error: 5')
        );
      }
    }
  );
}

指数バックオフ戦略を使用することで、サーバーに過負荷をかけることなくリトライを実行できます。

パフォーマンスを考慮した並行処理の実装テクニック

TypeScriptで非同期処理を扱う際、パフォーマンスを最適化するためのテクニックを知っておくことは重要です。以下では、並行処理を効率的に実装するための方法を紹介します。

Promise.allSettledの活用

ES2020で導入されたPromise.allSettled()は、すべてのPromiseが成功または失敗で解決されるまで待機し、それぞれの結果を配列で返します。これにより、一部の処理が失敗しても残りの処理を継続できます。

// Promise.allSettledの例
async function fetchAllUserDataSafely(userIds: string[]): Promise<{
  successful: User[];
  failed: { id: string; error: Error }[];
}> {
  const promises = userIds.map(id => 
    fetchUserData(id)
      .then(data => ({ status: 'fulfilled' as const, value: data, id }))
      .catch(error => ({ status: 'rejected' as const, reason: error, id }))
  );
  
  const results = await Promise.all(promises);
  
  const successful: User[] = [];
  const failed: { id: string; error: Error }[] = [];
  
  for (const result of results) {
    if (result.status === 'fulfilled') {
      successful.push(result.value);
    } else {
      failed.push({ id: result.id, error: result.reason });
    }
  }
  
  return { successful, failed };
}

// 使用例
async function processUsersWithoutFailingAll(userIds: string[]): Promise<void> {
  const results = await fetchAllUserDataSafely(userIds);
  
  console.log(`成功: ${results.successful.length}件, 失敗: ${results.failed.length}件`);
  
  if (results.failed.length > 0) {
    console.warn('以下のユーザーデータ取得に失敗しました:', results.failed.map(f => f.id).join(', '));
  }
  
  // 成功したデータだけで処理を続行
  processSuccessfulUsers(results.successful);
}

バッチ処理による最適化

大量のデータを処理する場合、バッチ処理を使用して段階的に処理することでメモリ使用量を抑えられます。

// バッチ処理の実装
async function processBatches<T, R>(
  items: T[],
  processFn: (batch: T[]) => Promise<R[]>,
  options: { batchSize?: number; delayBetweenBatches?: number } = {}
): Promise<R[]> {
  const { batchSize = 50, delayBetweenBatches = 0 } = options;
  const results: R[] = [];
  
  // データをバッチに分割
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    
    // バッチを処理
    const batchResults = await processFn(batch);
    results.push(...batchResults);
    
    // バッチ間に遅延を入れる(APIレート制限などに対応)
    if (delayBetweenBatches > 0 && i + batchSize < items.length) {
      await new Promise(resolve => setTimeout(resolve, delayBetweenBatches));
    }
    
    // 進捗状況を表示
    const progress = Math.min(i + batchSize, items.length) / items.length * 100;
    console.log(`処理進捗: ${progress.toFixed(1)}% (${i + batch.length}/${items.length})`);
  }
  
  return results;
}

// 使用例
async function updateAllUsers(users: User[]): Promise<UserUpdateResult[]> {
  return processBatches(
    users,
    async (batch) => {
      // 各バッチを並行して処理
      const updatePromises = batch.map(user => updateUserData(user));
      return Promise.all(updatePromises);
    },
    { batchSize: 20, delayBetweenBatches: 500 }
  );
}

Web Workersを使った並列処理

計算負荷の高い処理をメインスレッドでブロックせずに実行するには、Web Workersを活用できます。

// Web Workerを使った並列処理
function runInWorker<T, R>(
  workerFunction: (data: T) => R,
  data: T
): Promise<R> {
  // Web Workerで実行する関数を文字列化
  const fnString = workerFunction.toString();
  
  // Web Workerのコード
  const workerCode = `
    self.onmessage = function(e) {
      const fn = ${fnString};
      const result = fn(e.data);
      self.postMessage(result);
    }
  `;
  
  // Blob URLを作成
  const blob = new Blob([workerCode], { type: 'application/javascript' });
  const url = URL.createObjectURL(blob);
  
  return new Promise<R>((resolve, reject) => {
    const worker = new Worker(url);
    
    worker.onmessage = (e) => {
      resolve(e.data);
      worker.terminate();
      URL.revokeObjectURL(url);
    };
    
    worker.onerror = (e) => {
      reject(new Error(`Worker error: ${e.message}`));
      worker.terminate();
      URL.revokeObjectURL(url);
    };
    
    worker.postMessage(data);
  });
}

// 使用例
async function processLargeDataset(data: LargeDataset): Promise<ProcessedResult> {
  return runInWorker(
    (input) => {
      // 重い計算処理(メインスレッドをブロックしない)
      const result = {
        processed: input.items.map(item => heavyComputation(item)),
        summary: calculateSummary(input.items)
      };
      return result;
    },
    data
  );
}

Stream APIの活用

大きなデータを扱う場合、Stream APIを使用してメモリ効率を向上させることができます。

// Stream APIを使ったデータ処理
async function processLargeJSONStream(url: string): Promise<ProcessedData> {
  const response = await fetch(url);
  
  if (!response.ok) {
    throw new Error(`API error: ${response.status}`);
  }
  
  // レスポンスをストリームとして取得
  const reader = response.body!.getReader();
  const decoder = new TextDecoder();
  
  let jsonChunks = '';
  let processedItems = 0;
  const results: ProcessedItem[] = [];
  
  try {
    // チャンクごとにデータを処理
    while (true) {
      const { done, value } = await reader.read();
      
      if (done) {
        break;
      }
      
      // UTF-8デコード
      const chunk = decoder.decode(value, { stream: true });
      jsonChunks += chunk;
      
      // JSONの区切りを探す(この例では行区切りのJSONを想定)
      const lines = jsonChunks.split('\n');
      
      // 最後の不完全な行を除いて処理
      jsonChunks = lines.pop() || '';
      
      for (const line of lines) {
        if (line.trim()) {
          try {
            const item = JSON.parse(line);
            const processed = processItem(item);
            results.push(processed);
            processedItems++;
            
            // 進捗報告
            if (processedItems % 1000 === 0) {
              console.log(`${processedItems}件処理完了`);
            }
          } catch (e) {
            console.error('JSON解析エラー:', e);
          }
        }
      }
    }
    
    // 残りのチャンクを処理
    if (jsonChunks.trim()) {
      try {
        const item = JSON.parse(jsonChunks);
        const processed = processItem(item);
        results.push(processed);
      } catch (e) {
        console.error('最終チャンクのJSON解析エラー:', e);
      }
    }
    
    return {
      items: results,
      totalProcessed: processedItems
    };
  } finally {
    // リソースのクリーンアップ
    reader.releaseLock();
  }
}

Promise.anyの活用

複数の非同期処理のうち、最初に成功した結果だけが必要な場合はPromise.any()が便利です。

// Promise.anyの例(最も速いAPIからデータを取得)
async function fetchFromFastestMirror<T>(urls: string[]): Promise<T> {
  // 各URLからデータをフェッチするPromiseを作成
  const promises = urls.map(async (url, index) => {
    try {
      const startTime = Date.now();
      const response = await fetch(url);
      
      if (!response.ok) {
        throw new Error(`ミラー ${index + 1} エラー: ${response.status}`);
      }
      
      const data = await response.json();
      const endTime = Date.now();
      
      console.log(`ミラー ${index + 1} レスポンス時間: ${endTime - startTime}ms`);
      return data as T;
    } catch (error) {
      console.error(`ミラー ${index + 1} 失敗:`, error);
      throw error;
    }
  });
  
  try {
    // 最初に成功したPromiseの結果を返す
    return await Promise.any(promises);
  } catch (error) {
    // すべてのPromiseが失敗した場合
    if (error instanceof AggregateError) {
      throw new Error(`すべてのミラーが失敗しました: ${error.errors.map(e => e.message).join(', ')}`);
    }
    throw error;
  }
}

これらのテクニックを適切に組み合わせることで、TypeScriptアプリケーションの非同期処理を効率的かつパフォーマンスを意識して実装できます。並行処理の最適化は、特に大規模なデータ処理やリアルタイム機能を持つアプリケーションで重要になります。

おすすめの書籍

おすすめ記事

おすすめコンテンツ