TypeScriptでのasync/awaitを活用した繰り返し処理の最適な実装法

TypeScriptで非同期処理を扱う際、通常の同期処理と同じように繰り返し処理(ループ)を行うことがよくあります。しかし、非同期処理は同期処理とは異なる挙動をするため、正しい方法で処理を行わないと、意図しない結果を招くことがあります。本記事では、TypeScriptでの非同期処理における繰り返し処理を、async/awaitの仕組みとともに解説し、実務で役立つ具体例やベストプラクティスを紹介します。これにより、複雑な非同期処理でも効率的かつ正確に実装できるようになるでしょう。

目次

非同期処理とは


非同期処理とは、プログラムが特定のタスクを実行している間、他のタスクがブロックされずに進行できる処理方法のことです。一般的に、ファイルの読み書きやネットワークリクエスト、データベースアクセスなど、時間がかかる操作が非同期で行われます。これにより、アプリケーション全体がスムーズに動作し、ユーザーに快適な体験を提供できます。

非同期処理の重要性


非同期処理は、特に大規模なアプリケーションやリアルタイムデータ処理を必要とする環境で不可欠です。同期処理のようにタスクが順番待ちをせず、効率的なリソースの使用を可能にします。

async/awaitの基礎知識


async/awaitは、JavaScriptやTypeScriptで非同期処理をより直感的かつ可読性の高い方法で書くための構文です。従来のPromiseチェーンに比べ、同期処理と似た形式で非同期処理を記述できるため、コードが複雑になりにくく、エラーの発見やデバッグも容易になります。

async関数


asyncキーワードを使うことで、その関数が自動的にPromiseを返すようになります。関数内で非同期処理を行いたい場合、asyncを使うことで他の部分と連携しやすくなります。

async function fetchData() {
  return "データを取得しました";
}

この例では、fetchData()を呼び出すと、関数はPromiseを返します。

awaitキーワード


awaitは、Promiseの結果が返されるまでコードの実行を一時停止します。これにより、非同期処理の完了を待って次の行の処理が進行するため、同期処理に近い形で非同期操作を記述できます。

async function getData() {
  const result = await fetchData();
  console.log(result); // "データを取得しました"
}

このように、awaitを使うことで、Promiseを簡潔に処理できます。

繰り返し処理と非同期処理の関係


繰り返し処理(ループ)と非同期処理を組み合わせる場合、特有の挙動に注意が必要です。TypeScriptのforループやwhileループは同期的に動作するため、非同期処理を正しく扱わないと、意図しない順序で実行されてしまうことがあります。これは、非同期関数がすぐに戻らず、Promiseの結果が返ってくるまで待つ必要があるからです。

通常の繰り返し処理


同期的なループ処理では、各反復処理が順番に完了してから次に進むため、コードは上から下にそのまま実行されます。

for (let i = 0; i < 5; i++) {
  console.log(i);
}
// 出力: 0, 1, 2, 3, 4

この場合、ループは連続して順序通りに実行されますが、非同期処理をループ内に取り入れると、動作が変わります。

非同期処理とループ


非同期処理を含むループでは、各処理の完了を待つためにはawaitを適切に使う必要があります。そうしないと、全ての非同期処理が同時に実行されるか、順序が乱れる可能性があります。

for (let i = 0; i < 5; i++) {
  await asyncFunction(i);
  console.log(i);
}

ここでawaitを使用することで、各非同期処理が完了してから次の処理に進むため、期待通りに順序が保たれます。

async/awaitを用いた繰り返し処理の具体例


async/awaitを使用した繰り返し処理は、非同期関数をループ内で呼び出す際に特に役立ちます。非同期処理が完了するまで次の処理に進まないようにすることで、順序通りの結果が得られ、意図通りの処理フローを確保できます。ここでは、TypeScriptでのforループやforEachなど、様々な繰り返し処理での非同期処理の具体例を見ていきます。

forループとasync/await


forループ内で非同期処理を扱う場合、awaitを用いることで、各非同期関数が完了してから次の反復に進むようにします。以下は、データを逐次的に取得して処理する例です。

async function processItems(items: number[]) {
  for (let i = 0; i < items.length; i++) {
    const result = await asyncOperation(items[i]);
    console.log(`Item ${i}: ${result}`);
  }
}

async function asyncOperation(item: number): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => resolve(`Processed ${item}`), 1000);
  });
}

const items = [1, 2, 3, 4, 5];
processItems(items);

この例では、processItems関数がitems配列内の各要素に対して非同期処理を行い、awaitを用いることで、処理が逐次的に実行されます。

forEachとasync/await


forEachはJavaScriptで配列に対してよく使われるメソッドですが、非同期処理を行う際には注意が必要です。forEachawaitを待たずに処理を進めるため、非同期処理を逐次的に実行したい場合には向きません。

const items = [1, 2, 3, 4, 5];
items.forEach(async (item) => {
  const result = await asyncOperation(item);
  console.log(result);
});

このコードでは、forEachはすぐに次の反復処理に進むため、awaitが機能せず、非同期処理が並行して実行されます。そのため、順序通りの処理が保証されません。

for…ofループとasync/await


for...ofループはasync/awaitと組み合わせて使用する場合に非常に便利です。このループは、非同期処理を逐次的に実行する際に推奨される方法です。

async function processItems(items: number[]) {
  for (const item of items) {
    const result = await asyncOperation(item);
    console.log(`Processed: ${result}`);
  }
}

processItems([1, 2, 3, 4, 5]);

この例では、for...ofループが各非同期処理を順序通りに実行し、結果を期待通りに取得することができます。

whileループとasync/await


非同期処理をwhileループでも使えます。例えば、ある条件が満たされるまで繰り返し非同期処理を行う場合に、whileループ内でawaitを使うことができます。

async function processWhile() {
  let i = 0;
  while (i < 5) {
    const result = await asyncOperation(i);
    console.log(`Processed: ${result}`);
    i++;
  }
}

processWhile();

この例では、whileループ内でawaitを使用して非同期処理が完了するまで待ち、次の処理に進みます。

これらの例を通じて、async/awaitと繰り返し処理の連携を理解し、実務で役立つ非同期ループ処理の実装方法を学ぶことができます。

非同期関数の配列処理


非同期処理を複数のデータに対して行う場合、一度に全ての処理を実行するか、1つずつ順番に処理するかを選択する必要があります。特に、データが大量にある場合や、処理の順番が重要でない場合には、並列処理を行う方が効率的です。ここでは、TypeScriptで非同期関数を配列に対して効率的に処理する方法を解説します。

Promise.allでの並列処理


Promise.allは、複数のPromiseを並列に実行し、全てのPromiseが解決されるまで待機します。これにより、非同期処理が並行して実行され、処理時間を短縮できます。以下は、配列内の非同期処理を並列に実行する例です。

async function processItemsInParallel(items: number[]) {
  const promises = items.map(async (item) => {
    return await asyncOperation(item);
  });
  const results = await Promise.all(promises);
  results.forEach(result => console.log(result));
}

processItemsInParallel([1, 2, 3, 4, 5]);

この例では、items配列の各要素に対して非同期処理を並列で実行し、Promise.allで全ての結果をまとめて取得します。処理が並列に行われるため、逐次的に実行するよりも短時間で処理が完了します。

Promise.allSettledでの結果の取得


Promise.allSettledは、すべてのPromiseの処理結果を成功・失敗に関わらず取得できるため、エラーが発生しても他の処理が無駄になりません。以下の例では、並列処理の結果が成功・失敗の両方を考慮して取得されています。

async function processWithAllSettled(items: number[]) {
  const promises = items.map(item => asyncOperation(item));
  const results = await Promise.allSettled(promises);

  results.forEach(result => {
    if (result.status === "fulfilled") {
      console.log(`Success: ${result.value}`);
    } else {
      console.log(`Failed: ${result.reason}`);
    }
  });
}

processWithAllSettled([1, 2, 3, 4, 5]);

このコードでは、全てのPromiseが解決または拒否されるのを待ち、どの処理が成功し、どれが失敗したかを確認できます。これにより、エラーハンドリングが柔軟に行えるようになります。

逐次処理の実行


一方で、全ての非同期処理を順次実行し、それぞれが完了するまで待つ場合には、for...ofループを使うことが推奨されます。この方法は、処理の順番が重要な場合に使います。

async function processItemsSequentially(items: number[]) {
  for (const item of items) {
    const result = await asyncOperation(item);
    console.log(result);
  }
}

processItemsSequentially([1, 2, 3, 4, 5]);

この例では、for...ofループが使用され、各非同期処理が逐次的に実行されます。順序が大切な場合や、リソースの制限がある環境での処理には、このような方法が適しています。

非同期処理の最適化


並列処理と逐次処理は、それぞれにメリットとデメリットがあります。並列処理は処理時間を短縮できますが、リソースの使用量が増える可能性があります。逐次処理はリソースを抑えられるものの、全体の処理時間が長くなります。プロジェクトの要件に応じて、どちらの方法を選択するかを検討することが重要です。

非同期処理内でのエラーハンドリング


非同期処理を繰り返し実行する際、エラーハンドリングは非常に重要な要素です。特に、繰り返し処理内で複数の非同期関数を呼び出す場合、どのようにエラーをキャッチし、処理の流れを維持するかを適切に考慮する必要があります。TypeScriptでは、try/catchブロックやPromiseのエラー処理機能を使用して、非同期処理のエラーを効率的に管理できます。

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


async/await構文でエラーを処理するためには、try/catchブロックを使うのが一般的です。これにより、同期処理と同じようにエラーハンドリングを行うことができ、コードがシンプルになります。

async function processItemWithErrorHandling(item: number) {
  try {
    const result = await asyncOperation(item);
    console.log(`Processed: ${result}`);
  } catch (error) {
    console.error(`Error processing item ${item}:`, error);
  }
}

async function processItems(items: number[]) {
  for (const item of items) {
    await processItemWithErrorHandling(item);
  }
}

processItems([1, 2, 3, 4, 5]);

この例では、各非同期処理に対してtry/catchを使用することで、エラーが発生した場合でも処理の流れが止まることなく、次のアイテムへ処理を進めることができます。エラーが発生したアイテムに関してはエラーメッセージをログに残すことで、問題の特定も容易になります。

Promise.allでのエラーハンドリング


Promise.allを使った並列処理では、1つのPromiseが失敗すると、全ての処理が拒否されるという挙動があります。この場合、個別のエラーを処理するためにtry/catchではなく、Promise.allSettledのようなアプローチを使う方が柔軟です。

async function processItemsInParallelWithHandling(items: number[]) {
  const promises = items.map(async (item) => {
    try {
      return await asyncOperation(item);
    } catch (error) {
      return `Error processing item ${item}: ${error}`;
    }
  });

  const results = await Promise.all(promises);
  results.forEach(result => console.log(result));
}

processItemsInParallelWithHandling([1, 2, 3, 4, 5]);

このコードでは、各Promiseでエラーが発生した場合、そのエラーを個別に処理し、他の処理には影響を与えないようにしています。これにより、1つのエラーが原因で全ての並列処理が停止することを防ぐことができます。

エラーの再試行処理


非同期処理が外部APIへのリクエストやファイル操作などを含む場合、一時的なエラーによって処理が失敗することがあります。こういったケースでは、エラーが発生した処理を再試行する戦略を取り入れることが有効です。

async function retryOperation(item: number, retries: number): Promise<string> {
  for (let i = 0; i < retries; i++) {
    try {
      return await asyncOperation(item);
    } catch (error) {
      console.log(`Retry ${i + 1} failed for item ${item}`);
    }
  }
  throw new Error(`Failed to process item ${item} after ${retries} attempts`);
}

async function processItemsWithRetry(items: number[], retries: number) {
  for (const item of items) {
    try {
      const result = await retryOperation(item, retries);
      console.log(result);
    } catch (error) {
      console.error(error.message);
    }
  }
}

processItemsWithRetry([1, 2, 3, 4, 5], 3);

この例では、各アイテムに対して非同期処理を最大で3回まで再試行し、それでも失敗した場合にはエラーを報告します。これにより、非同期処理の信頼性が向上し、一時的なエラーによる失敗を防ぐことができます。

エラーハンドリングのベストプラクティス

  • 非同期処理におけるエラーハンドリングは、try/catchを基本にしつつ、Promise.allや並列処理で柔軟に対処します。
  • 重大なエラーが発生した場合でも、他の処理に影響を与えないようにすることが重要です。
  • 再試行戦略やタイムアウトを取り入れ、予測不能なエラーに対処する準備を整えることが望ましいです。

エラーハンドリングを適切に行うことで、非同期処理の信頼性と堅牢性が大幅に向上します。

繰り返し処理と並列処理の違い


非同期処理において、繰り返し処理と並列処理の選択は重要なポイントです。両者は異なる動作をし、用途によって適切な使い方が求められます。ここでは、繰り返し処理と並列処理の違いを明確にし、それぞれの利点と課題、使いどころを解説します。

逐次処理 (繰り返し処理)


逐次処理(シーケンシャル処理)は、各タスクが順番に実行される処理方式です。1つのタスクが完了するまで次のタスクに進まず、処理の順序が保証されます。forループやwhileループで非同期処理を行う場合、awaitを使って逐次的に処理を実行します。

async function sequentialProcessing(items: number[]) {
  for (const item of items) {
    const result = await asyncOperation(item);
    console.log(`Processed item: ${result}`);
  }
}

sequentialProcessing([1, 2, 3, 4, 5]);

この例では、各アイテムが順番に処理され、全てが完了するまで次の処理に進みません。

逐次処理のメリット

  • 順序の保証:タスクが順番通りに実行され、処理の順序が重要な場合に適しています。
  • シンプルなエラーハンドリングtry/catchを用いたエラーハンドリングが容易で、エラーが発生しても次の処理に影響を与えにくいです。

逐次処理のデメリット

  • 処理速度の低下:各タスクが完了するまで次に進まないため、全体の処理時間が長くなります。大量の非同期処理が必要な場合には非効率です。

並列処理


並列処理では、複数のタスクが同時に実行され、すべてのタスクが終了するまで待つことができます。並列処理はPromise.allなどを使用して実現できます。

async function parallelProcessing(items: number[]) {
  const promises = items.map(item => asyncOperation(item));
  const results = await Promise.all(promises);
  results.forEach(result => console.log(`Processed item: ${result}`));
}

parallelProcessing([1, 2, 3, 4, 5]);

この例では、全ての非同期処理が並行して実行され、全ての処理が完了した後に結果が表示されます。

並列処理のメリット

  • 高速処理:複数のタスクが同時に実行されるため、処理時間が大幅に短縮されます。特に大量の非同期処理を扱う場合に効果的です。
  • リソースの最大活用:サーバーやAPIリクエストなど、待機時間が長い処理でもリソースを無駄にせず、他のタスクを並行して進められます。

並列処理のデメリット

  • 順序が保証されない:各タスクが並行して実行されるため、処理の完了順序が不定となり、順序が重要な場合には不向きです。
  • 複雑なエラーハンドリング:1つのタスクが失敗した場合、他のタスクにも影響を与える可能性があり、エラー処理が難しくなることがあります。

逐次処理と並列処理の使い分け

  • 逐次処理が適している場合:処理の順序が重要であり、各タスクの完了を待ってから次に進む必要がある場合。例えば、ステップバイステップでデータを処理するような状況では、逐次処理が適しています。
  • 並列処理が適している場合:処理順序が重要でないか、処理時間を短縮することが最優先の場合。例えば、複数のAPIリクエストや非同期ファイル操作などを一度に実行する場合には、並列処理が効果的です。

注意点:リソースの限界と最適化


並列処理はリソースを最大限に活用できる一方で、同時に実行されるタスクが多すぎると、サーバーやクライアントのリソースが限界に達することがあります。大量の非同期タスクを実行する際は、適切に制御し、リソースの使用を最適化することが必要です。

逐次処理と並列処理の違いを理解し、状況に応じて適切な方法を選択することで、効率的な非同期処理を実現することができます。

実務での応用例


非同期処理と繰り返し処理を組み合わせた具体的な応用例を実務の観点から見ていきましょう。TypeScriptでのasync/awaitを活用する場面は、APIからのデータ取得、データベースとのやり取り、ファイル操作など、様々なユースケースがあります。ここでは、非同期処理を活用したいくつかの具体例を紹介し、それぞれのシナリオでのベストプラクティスを考察します。

APIからの大量データ取得


APIを使って外部から大量のデータを取得し、それを処理するケースは非常に一般的です。例えば、ページネーションを使って数百件に及ぶデータを取得し、それらを1つずつ処理する必要がある場合、async/awaitで繰り返し処理を実装することができます。

async function fetchAndProcessData() {
  const dataUrls = ['https://api.example.com/data1', 'https://api.example.com/data2', 'https://api.example.com/data3'];

  for (const url of dataUrls) {
    try {
      const response = await fetch(url);
      const data = await response.json();
      processData(data);
    } catch (error) {
      console.error(`Failed to fetch data from ${url}:`, error);
    }
  }
}

fetchAndProcessData();

この例では、各APIエンドポイントにリクエストを送り、データを取得して逐次的に処理しています。try/catchを使ってエラーハンドリングを行い、どのAPIリクエストが失敗したかを記録します。

ベストプラクティス

  • 並列処理が可能な場合、Promise.allで全てのリクエストを並行して実行することでパフォーマンスを最適化します。
  • エラーハンドリングを適切に実装し、個々のリクエストが失敗しても全体の処理が中断しないようにすることが重要です。

ファイルシステムの操作


ファイルの読み書きも非同期処理を伴う操作です。例えば、複数のファイルを一度に読み込み、その内容を処理する場合、async/awaitを使用した繰り返し処理が役立ちます。

import { promises as fs } from 'fs';

async function processFiles(files: string[]) {
  for (const file of files) {
    try {
      const content = await fs.readFile(file, 'utf-8');
      console.log(`Content of ${file}: ${content}`);
    } catch (error) {
      console.error(`Error reading file ${file}:`, error);
    }
  }
}

const fileNames = ['file1.txt', 'file2.txt', 'file3.txt'];
processFiles(fileNames);

この例では、複数のファイルを逐次的に読み込み、その内容を出力しています。ファイルが存在しない場合や、アクセス権限の問題など、try/catchでエラーハンドリングを行っています。

ベストプラクティス

  • ファイル操作はI/Oが発生するため、並列処理を行うことで効率を向上できますが、リソース負荷に注意する必要があります。適切な並列数を管理するため、タスクキューを使うと効果的です。

データベースへの複数クエリ実行


複数のデータベースクエリを実行し、その結果を処理する場合にも、async/awaitを使って繰り返し処理を行えます。たとえば、複数のテーブルからデータを取得し、それぞれの結果を組み合わせて処理する例を見てみましょう。

async function fetchUserData(userIds: number[]) {
  for (const id of userIds) {
    try {
      const user = await getUserFromDatabase(id);
      const orders = await getUserOrders(id);
      console.log(`User: ${user.name}, Orders: ${orders.length}`);
    } catch (error) {
      console.error(`Error fetching data for user ${id}:`, error);
    }
  }
}

fetchUserData([1, 2, 3]);

この例では、データベースからユーザーデータとその注文履歴を取得し、逐次的に処理しています。各クエリが非同期であるため、awaitを使って処理が完了するまで待機しています。

ベストプラクティス

  • データベースへのアクセスはリソースに負担がかかるため、並列処理を行う際には接続数やリソースの制限に注意する必要があります。
  • トランザクションを利用して、複数のクエリが確実に成功するようにエラーハンドリングを行うと、信頼性が向上します。

まとめ


非同期処理と繰り返し処理の組み合わせは、多くの実務シナリオで活用されます。APIのデータ取得、ファイル操作、データベースクエリなど、様々なケースでasync/awaitを用いた効率的な非同期処理を実装することが可能です。適切なエラーハンドリングと並列処理のバランスを考慮し、処理の最適化を図ることが重要です。

ベストプラクティス


非同期処理と繰り返し処理を組み合わせる際には、いくつかのベストプラクティスを守ることで、パフォーマンスやコードの可読性、エラーハンドリングを向上させることができます。ここでは、実務で役立つベストプラクティスをいくつか紹介します。

1. 必要に応じて逐次処理と並列処理を使い分ける


非同期処理において、逐次処理と並列処理の選択は非常に重要です。処理の順序が重要な場合は逐次処理を、パフォーマンスを優先したい場合や順序が重要でない場合は並列処理を採用します。

  • 逐次処理: for...ofawaitを使って1つずつ処理する。
  • 並列処理: Promise.allPromise.allSettledを使って複数の非同期処理を同時に実行する。

並列処理はパフォーマンスが向上しますが、リソースの使用量が増える可能性があります。これを踏まえて、タスクの特性に応じて最適な方法を選択しましょう。

2. 適切なエラーハンドリングを行う


非同期処理ではエラーが発生する可能性が高いため、適切なエラーハンドリングを実装することが重要です。try/catchブロックを使うことで、処理中にエラーが発生した場合でも次の処理に進むことができます。

  • 個別のエラーハンドリング: 各非同期処理ごとにtry/catchを使ってエラーハンドリングを行い、エラーが他の処理に影響を与えないようにします。
  • 全体的なエラーハンドリング: 複数の非同期処理を並行して実行する場合は、Promise.allSettledを使うことで、成功した処理と失敗した処理を個別に確認できます。

3. 再試行ロジックを取り入れる


ネットワークエラーや外部サービスの問題により、非同期処理が失敗する場合があります。そのため、特定の非同期処理を再試行する仕組みを導入することで、信頼性を高めることができます。

async function retryAsyncOperation(operation: () => Promise<any>, retries: number): Promise<any> {
  for (let i = 0; i < retries; i++) {
    try {
      return await operation();
    } catch (error) {
      console.error(`Retry ${i + 1} failed`);
    }
  }
  throw new Error('Operation failed after multiple retries');
}

再試行回数を設定することで、非同期処理が失敗しても、一定回数までリトライを行うことができます。

4. リソースの制限を考慮する


並列処理を無制限に実行すると、システムのリソースを大量に消費してしまう場合があります。例えば、APIリクエストのレート制限や、ファイルアクセスのI/O負荷などを考慮する必要があります。適切に並列処理を制御するために、タスクキューやスロットリング技術を導入することを推奨します。

async function limitConcurrentTasks(tasks: (() => Promise<any>)[], limit: number): Promise<any[]> {
  const results = [];
  const executing = [];

  for (const task of tasks) {
    const promise = task().then(result => {
      executing.splice(executing.indexOf(promise), 1);
      return result;
    });
    executing.push(promise);
    if (executing.length >= limit) {
      await Promise.race(executing);
    }
  }

  return Promise.all(executing);
}

この例では、並列で実行されるタスクの数を制限し、システムへの負荷を抑えることができます。

5. 一貫性のあるコードスタイルを維持する


非同期処理は複雑になりやすいので、可読性の高いコードを維持するために、async/awaitPromiseの使用方法を統一することが重要です。統一されたコードスタイルは、他の開発者がコードを理解しやすくし、メンテナンス性を向上させます。

  • async/awaitを使う場合は、同期的なコードに近いスタイルで書くことで可読性が向上します。
  • Promiseチェーンを使用する場合は、明確なエラーハンドリングやコメントを追加して、非同期フローが分かりやすくなるようにしましょう。

6. パフォーマンスモニタリングを行う


非同期処理は、並列実行やリソース管理の観点でパフォーマンスに大きな影響を与えることがあります。したがって、非同期処理の実行時間やリソースの消費量をモニタリングし、最適化ポイントを特定することが重要です。適切なロギングやパフォーマンス分析ツールを使用して、ボトルネックを発見し、改善を行うことが推奨されます。

これらのベストプラクティスを守ることで、効率的で信頼性の高い非同期処理を実現でき、実務においてもスムーズな開発プロセスを維持することができます。

演習問題


ここでは、非同期処理と繰り返し処理の理解を深めるための実践的な演習問題を紹介します。これらの問題を通じて、async/awaitや並列処理、エラーハンドリングの実装方法を実際に体験し、より深い理解を得られるようにしましょう。

演習1: APIからのデータを逐次処理する


複数のAPIエンドポイントにリクエストを送り、それらのデータを1つずつ取得して処理する関数を実装してください。各APIリクエストが成功するたびに、取得したデータをコンソールに出力します。

要件:

  • async/awaitを使って、APIリクエストが逐次的に行われるようにします。
  • エラーハンドリングを実装し、リクエストが失敗した場合でも次のリクエストに進めるようにします。

ヒント:

const urls = ['https://api.example.com/data1', 'https://api.example.com/data2', 'https://api.example.com/data3'];

async function fetchSequentialData(urls: string[]) {
  for (const url of urls) {
    try {
      const response = await fetch(url);
      const data = await response.json();
      console.log(`Data from ${url}:`, data);
    } catch (error) {
      console.error(`Failed to fetch data from ${url}:`, error);
    }
  }
}

fetchSequentialData(urls);

演習2: 並列処理でファイルを読み込む


複数のファイルを並列で読み込み、それらの内容をまとめて処理する関数を作成してください。ファイルの内容が取得できたら、各ファイルの内容をコンソールに表示します。

要件:

  • Promise.allを使って、全てのファイル読み込み処理を並列で実行します。
  • ファイルの内容を表示する際に、それぞれのファイル名も一緒に出力します。

ヒント:

import { promises as fs } from 'fs';

const fileNames = ['file1.txt', 'file2.txt', 'file3.txt'];

async function readFilesInParallel(fileNames: string[]) {
  const readPromises = fileNames.map(async (fileName) => {
    const content = await fs.readFile(fileName, 'utf-8');
    return { fileName, content };
  });

  const filesData = await Promise.all(readPromises);
  filesData.forEach(({ fileName, content }) => {
    console.log(`Content of ${fileName}: ${content}`);
  });
}

readFilesInParallel(fileNames);

演習3: APIリクエストの再試行ロジックを実装する


特定のAPIリクエストが失敗した場合に、最大3回まで再試行する非同期関数を実装してください。再試行のたびに、試行回数をコンソールに出力し、最終的に成功した場合はデータを、失敗した場合はエラーメッセージを表示します。

要件:

  • 各リクエストが失敗した際、再試行するロジックを実装します。
  • 再試行回数が上限に達しても成功しない場合、エラーメッセージを表示します。

ヒント:

async function fetchWithRetries(url: string, retries: number) {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`Error: ${response.statusText}`);
      const data = await response.json();
      console.log(`Success on attempt ${i + 1}:`, data);
      return;
    } catch (error) {
      console.log(`Attempt ${i + 1} failed`);
    }
  }
  console.error(`Failed to fetch data from ${url} after ${retries} attempts`);
}

fetchWithRetries('https://api.example.com/data', 3);

演習4: タスクの並列数を制限して処理する


並列で実行できるタスクの数を制限し、順次処理を行う関数を実装してください。例えば、5つのタスクがある場合、同時に2つのタスクだけが並列で実行されるように制御します。

要件:

  • 同時に実行できるタスク数を制限し、制限内で処理を進めます。
  • すべてのタスクが完了するまで待機します。

ヒント:

async function limitConcurrentTasks(tasks: (() => Promise<any>)[], limit: number) {
  const results = [];
  const executing = [];

  for (const task of tasks) {
    const promise = task().then(result => {
      executing.splice(executing.indexOf(promise), 1);
      return result;
    });
    executing.push(promise);
    if (executing.length >= limit) {
      await Promise.race(executing);
    }
  }

  return Promise.all(executing);
}

// Example usage
const tasks = [
  () => asyncOperation(1),
  () => asyncOperation(2),
  () => asyncOperation(3),
  () => asyncOperation(4),
  () => asyncOperation(5)
];

limitConcurrentTasks(tasks, 2).then(results => {
  console.log('All tasks completed:', results);
});

これらの演習問題を通じて、非同期処理の実装方法をより実践的に学ぶことができます。各問題に取り組むことで、繰り返し処理、並列処理、エラーハンドリング、リトライ戦略の使い方に習熟することができるでしょう。

まとめ


本記事では、TypeScriptにおける非同期処理と繰り返し処理の組み合わせについて、async/awaitの基礎から、逐次処理と並列処理の違い、実務での応用例、エラーハンドリング、再試行ロジック、並列数の制御まで詳しく解説しました。適切な処理方法を選択することで、パフォーマンスと信頼性を両立させる非同期プログラムを実装できるようになります。演習問題を通じてさらに理解を深め、実践で活用してください。

コメント

コメントする

目次