TypeScriptにおけるPromiseの基本的な使い方を徹底解説

TypeScriptにおいて、非同期処理は多くの場面で重要な役割を果たします。非同期処理を管理するための標準的な方法として「Promise」があります。Promiseを使用することで、コールバック地獄と呼ばれる複雑なコードの構造を避け、可読性の高いコードを実現できます。本記事では、TypeScriptにおけるPromiseの基本的な使い方から、応用的なテクニックまでを詳細に解説し、非同期処理の効果的な管理方法を学びます。Promiseの仕組みや使用例を通じて、TypeScriptのコードの効率を高める方法を理解していきましょう。

目次

Promiseとは何か

Promiseは、JavaScriptやTypeScriptにおける非同期処理を扱うためのオブジェクトです。非同期処理とは、処理の結果が即座に得られない場合に用いられる概念で、サーバーからデータを取得するAPIリクエストや、ファイルの読み込み、タイマーの実行など、時間がかかる処理を待たずに他の作業を並行して行うことが可能です。

Promiseは3つの状態を持ちます:

  1. Pending(保留):Promiseがまだ結果を返していない状態。
  2. Fulfilled(成功):非同期処理が成功し、結果が得られた状態。
  3. Rejected(失敗):非同期処理が失敗し、エラーが発生した状態。

Promiseを利用することで、非同期処理の結果を受け取った後の動作を定義し、エラーハンドリングも効率的に行えるようになります。

Promiseの基本構文

Promiseの基本構文は、非同期処理をラップして、その結果を管理するために使用されます。Promiseコンストラクタを利用して、新しいPromiseを作成し、その中で非同期処理を行います。Promiseの基本的な構文は以下のようになります。

const promise = new Promise((resolve, reject) => {
  // 非同期処理をここに記述
  const success = true;

  if (success) {
    resolve("処理が成功しました");
  } else {
    reject("処理が失敗しました");
  }
});

このコードでは、resolveはPromiseが成功した場合に呼び出される関数で、rejectは失敗した場合に呼び出されます。Promiseを使用することで、非同期処理が完了した後にどのように処理を進めるかを明確に記述できます。

then()とcatch()の基本的な使い方

Promiseの結果を処理するためには、then()catch()メソッドを使用します。then()は非同期処理が成功した場合に、catch()は失敗した場合に呼ばれます。

promise
  .then((result) => {
    console.log(result); // "処理が成功しました"が表示される
  })
  .catch((error) => {
    console.error(error); // "処理が失敗しました"が表示される
  });

このように、Promiseの基本構文では、非同期処理の結果を簡潔かつ明確に扱うことができます。

then()とcatch()の使い方

then()catch()は、Promiseを使って非同期処理が成功した場合や失敗した場合に、それぞれの結果を処理するために利用されます。Promiseチェーンを構築することで、非同期処理の流れをスムーズに制御できるのが特徴です。

then()メソッド

then()メソッドは、Promiseが成功したときに呼び出され、resolve()で渡された値を受け取ります。複数のthen()をつなげることで、連続する非同期処理を行うことが可能です。

const fetchData = new Promise((resolve, reject) => {
  const data = { name: "John", age: 30 };
  resolve(data); // 非同期処理が成功した場合
});

fetchData
  .then((result) => {
    console.log(result.name); // "John" が表示される
    return result.age;
  })
  .then((age) => {
    console.log(age); // 30 が表示される
  });

このように、then()メソッドで非同期処理の結果を受け取り、その結果に基づいて次の処理を続けることができます。

catch()メソッド

catch()メソッドは、Promiseが失敗したとき、つまりreject()が呼び出された場合にエラーをキャッチし、エラーハンドリングを行うために使われます。

const fetchDataWithError = new Promise((resolve, reject) => {
  reject("データ取得エラー"); // 非同期処理が失敗した場合
});

fetchDataWithError
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.error(error); // "データ取得エラー" が表示される
  });

Promiseチェーンの途中でエラーが発生した場合、catch()はその時点からすべての後続処理をスキップし、エラーハンドリングに移ります。

Promiseチェーンの活用

then()catch()は複数回使用でき、Promiseチェーンを作成することで、複数の非同期処理を順序よく実行できます。例えば、複数の非同期処理を直列に処理する場合、それぞれのthen()で次の非同期処理を呼び出すことができます。

fetchData
  .then((result) => {
    return new Promise((resolve, reject) => {
      resolve(`ユーザー名は ${result.name} です`);
    });
  })
  .then((message) => {
    console.log(message); // "ユーザー名は John です" が表示される
  })
  .catch((error) => {
    console.error("エラー:", error);
  });

Promiseチェーンを活用することで、非同期処理のフローを明確にし、エラーハンドリングも一箇所で管理できるようになります。

Promiseを使った非同期処理の実例

Promiseを使用する最も一般的なケースの一つが、APIリクエストです。実際のアプリケーションでは、サーバーからデータを取得する際に、非同期処理が必要になります。ここでは、Promiseを使ってAPIリクエストを行い、その結果を処理する実例を見てみましょう。

APIリクエストの実装例

次のコードは、fetch()を使ってAPIからデータを取得し、Promiseによってその結果を管理する方法です。fetch()はデフォルトでPromiseを返すため、then()catch()を使って結果やエラーを処理します。

const apiUrl = "https://jsonplaceholder.typicode.com/posts/1";

fetch(apiUrl)
  .then((response) => {
    if (!response.ok) {
      throw new Error("ネットワークエラー");
    }
    return response.json(); // JSON形式のデータを取得
  })
  .then((data) => {
    console.log("取得したデータ:", data); // APIから取得したデータを表示
  })
  .catch((error) => {
    console.error("エラーが発生しました:", error);
  });

この例では、以下の流れでPromiseを使って非同期処理を行います:

  1. fetch(apiUrl)がAPIリクエストを送信し、Promiseを返します。
  2. then()を使用して、レスポンスが成功したかどうかを確認します。失敗した場合は、throwを使ってエラーを投げ、catch()に渡します。
  3. レスポンスが正常であれば、response.json()を呼び出して、JSON形式のデータをパースします。
  4. その後、取得したデータを処理し、結果をコンソールに表示します。
  5. 万が一、エラーが発生した場合は、catch()でエラーメッセージを表示します。

非同期処理をPromiseで管理する利点

Promiseを使用することで、非同期処理が完了したタイミングでその結果を簡単に扱うことができ、エラーハンドリングも容易です。このAPIリクエストの例では、データ取得の成功時にはthen()で結果を処理し、失敗時にはcatch()でエラーを適切に処理しています。

また、fetch()のようなPromiseを返す関数を使うことで、非同期処理のコードがより読みやすく、直感的になります。これにより、APIリクエストがいつ完了するかを意識する必要がなく、コード全体がシンプルでメンテナンスしやすくなります。

他の非同期処理の例

Promiseは、APIリクエスト以外にも、ファイルの読み込み、タイマー処理、データベース操作など、さまざまな非同期処理に利用できます。例えば、タイマー処理をPromiseでラップすることも可能です。

const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

delay(2000).then(() => {
  console.log("2秒経過しました");
});

この例では、delay()関数が指定した時間(ここでは2秒)を待った後にthen()が実行されます。このようにPromiseを使うことで、非同期処理を簡単に管理することができます。

Promiseを使った非同期処理の例を理解することで、より実践的なアプリケーション開発に活用できるでしょう。

async/awaitの基本とPromiseとの関係

async/awaitは、Promiseを使った非同期処理をさらにシンプルで読みやすい形で記述できる構文です。async/awaitは、Promiseのチェーンを使わずに、同期処理のようにコードを記述できるため、複雑な非同期処理も直感的に書くことができます。async/awaitは実際にはPromiseを裏で利用しているため、Promiseとの相互関係も理解する必要があります。

async関数の基本

asyncキーワードを使って関数を定義すると、その関数は常にPromiseを返すようになります。つまり、関数の結果は自動的にPromiseにラップされ、非同期処理の管理がしやすくなります。

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

fetchData().then((data) => {
  console.log(data); // "データが取得されました" が表示される
});

この例では、fetchData()関数がasyncとして定義されているため、関数はPromiseを返し、その結果はthen()で受け取ることができます。

awaitキーワードの使い方

awaitキーワードは、Promiseの結果を待つために使います。awaitを使うことで、Promiseが解決(成功)するまでコードの実行を一時停止し、まるで同期処理のように結果を取得できます。これにより、複雑なPromiseチェーンを避け、シンプルで直感的なコードを書くことができます。

async function fetchData() {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
  const data = await response.json();
  console.log(data);
}

fetchData();

この例では、awaitを使ってfetch()が返すPromiseが解決されるまで待機し、その後response.json()を使ってレスポンスデータをパースしています。これにより、同期処理のようにAPIリクエストの結果を処理できます。

async/awaitを使ったエラーハンドリング

Promiseでエラーを処理する場合、catch()を使用していましたが、async/awaitでは、通常のtry...catch構文を使ってエラーハンドリングができます。これにより、同期コードと同様にエラー処理が可能です。

async function fetchData() {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
    if (!response.ok) {
      throw new Error("ネットワークエラー");
    }
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error("エラーが発生しました:", error);
  }
}

fetchData();

この例では、try...catchを使ってawaitで待機している処理のエラーハンドリングを行っています。APIリクエストが失敗した場合やレスポンスが正常でない場合は、エラーがcatchブロックに渡され、エラーメッセージが表示されます。

async/awaitの利点

  • 可読性の向上: 複数の非同期処理を直列に行う場合でも、同期処理のように記述できるため、コードがシンプルで読みやすくなります。
  • エラーハンドリングの一元化: try...catchを使うことで、複数のthen()catch()を使うよりも、エラーハンドリングが簡単になります。
  • 非同期処理の管理が容易: 複雑なPromiseチェーンを避け、非同期処理を直感的に管理できるため、デバッグや保守が容易になります。

async/awaitを使用することで、Promiseをベースとした非同期処理をよりシンプルに記述でき、コードの可読性や保守性を向上させることができます。Promiseを使いこなしている場合でも、この構文を導入することで、さらに効率的に非同期処理を扱えるようになるでしょう。

Promise.all()とPromise.race()

JavaScriptやTypeScriptでは、複数の非同期処理を同時に実行する場合、Promiseオブジェクトを使って効率よく結果を管理することができます。そのために使用される代表的なメソッドが、Promise.all()Promise.race()です。これらを活用することで、複数のPromiseの結果をまとめて扱うことができ、非同期処理を効率化できます。

Promise.all()の使い方

Promise.all()は、複数のPromiseを並列で実行し、それらがすべて成功した場合に結果を返します。全てのPromiseが解決されるまで待機し、その結果を一括して受け取ることができるため、複数の非同期処理をまとめて処理するのに非常に便利です。

const promise1 = fetch("https://jsonplaceholder.typicode.com/posts/1");
const promise2 = fetch("https://jsonplaceholder.typicode.com/posts/2");

Promise.all([promise1, promise2])
  .then((responses) => {
    return Promise.all(responses.map((response) => response.json()));
  })
  .then((data) => {
    console.log("取得したデータ:", data);
  })
  .catch((error) => {
    console.error("エラーが発生しました:", error);
  });

この例では、2つのfetch()リクエストをPromise.all()で並列に実行し、両方のレスポンスを待っています。すべてのPromiseが解決された後、結果をJSON形式に変換してデータを処理しています。万が一、いずれかのPromiseが失敗した場合、catch()ブロックでエラーハンドリングが行われます。

Promise.all()の特徴

  • 全てのPromiseが成功するまで待つ: Promise.all()は、全てのPromiseが解決されるまで処理を一時停止します。1つでも失敗すると、即座にcatch()に移行します。
  • 複数の非同期処理をまとめて管理: 複数の非同期処理を一度に処理するための強力な手段です。例えば、複数のAPIリクエストや、複数ファイルの読み込み処理などに活用できます。

Promise.race()の使い方

Promise.race()は、複数のPromiseのうち最初に解決(または拒否)されたものの結果を返します。全てのPromiseが解決されるのを待つのではなく、どれか1つのPromiseが完了した時点でその結果を取得するため、レスポンスの速さが求められる場合に使用されます。

const slowPromise = new Promise((resolve) => {
  setTimeout(() => resolve("遅い処理が完了しました"), 3000);
});

const fastPromise = new Promise((resolve) => {
  setTimeout(() => resolve("速い処理が完了しました"), 1000);
});

Promise.race([slowPromise, fastPromise])
  .then((result) => {
    console.log(result); // "速い処理が完了しました" が表示される
  })
  .catch((error) => {
    console.error("エラーが発生しました:", error);
  });

この例では、slowPromisefastPromiseのどちらかが先に解決されるまで待機し、最初に解決したfastPromiseの結果がthen()で処理されます。Promise.race()は最も早く完了するPromiseの結果のみを返すため、非同期処理の速度を重視するシナリオで有効です。

Promise.race()の特徴

  • 最初に解決されたPromiseを返す: 複数のPromiseのうち、最も早く解決または拒否されたPromiseの結果を返します。
  • 速さ重視の処理に有効: 例えば、複数のデータソースから最も早く結果を返すものを使用したい場合など、競争的な処理に適しています。

Promise.all()とPromise.race()の使いどころ

  • Promise.all(): 全ての非同期処理が完了するまで待ち、その結果を一括で処理する必要がある場合に適しています。例えば、複数のAPIからのデータを同時に取得して、一つの画面に表示する際などに使用します。
  • Promise.race(): 複数のPromiseのうち、一番早く結果が得られたものを優先したい場合に利用します。タイムアウト処理や、複数のデータソースを用いた競争処理に効果的です。

これらのメソッドを適切に使い分けることで、非同期処理の効率を高め、さまざまなシナリオに対応できる非同期処理フローを構築することができます。

Promiseのエラーハンドリング

非同期処理では、エラーが発生した場合の処理が非常に重要です。Promiseを使うことで、エラーハンドリングも効率的に行うことができます。Promiseでエラーハンドリングを適切に行うことで、システムの信頼性が向上し、予期しない動作を防ぐことができます。

catch()によるエラーハンドリング

Promiseにおけるエラーは、catch()メソッドで処理されます。catch()は、Promiseが失敗(rejectされた)場合や、then()メソッド内でエラーが発生した場合に呼び出されます。catch()はチェーンの最後に置くのが一般的で、どのPromiseでエラーが発生してもキャッチできます。

const fetchData = new Promise((resolve, reject) => {
  const errorOccurred = true; // エラーが発生したと仮定
  if (errorOccurred) {
    reject("データ取得に失敗しました");
  } else {
    resolve("データ取得に成功しました");
  }
});

fetchData
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.error("エラー:", error); // "エラー: データ取得に失敗しました" が表示される
  });

この例では、fetchDataのPromiseが失敗したため、catch()が呼び出されてエラーメッセージが表示されます。Promise内で発生したエラーは、すべてcatch()で受け取ることができます。

then()内でのエラーハンドリング

通常、Promiseのエラーはcatch()で処理しますが、then()メソッド内でもエラーを処理することが可能です。たとえば、then()メソッド内で発生したエラーも自動的にキャッチされ、catch()に渡されます。

fetchData
  .then((result) => {
    // ここでエラーが発生すると自動的にcatch()に渡される
    throw new Error("処理中にエラーが発生しました");
  })
  .catch((error) => {
    console.error("キャッチされたエラー:", error.message); // "キャッチされたエラー: 処理中にエラーが発生しました"
  });

このように、Promiseチェーン内でのエラーは、catch()がすべて受け取り、最後に一括して処理できます。これにより、非同期処理中にどこでエラーが発生しても、統一されたエラーハンドリングが可能です。

finally()でのクリーンアップ処理

Promiseには、finally()メソッドも存在します。このメソッドは、Promiseが成功(resolve)でも失敗(reject)でも、必ず実行されます。クリーンアップ処理や、リソースの解放が必要な場合にfinally()を使用するのが有効です。

fetchData
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.error("エラー:", error);
  })
  .finally(() => {
    console.log("非同期処理が完了しました"); // 成功・失敗に関係なく実行される
  });

この例では、finally()がPromiseの状態に関係なく実行されるため、クリーンアップ操作やローディング表示の解除など、どちらの結果でも必要な処理を記述できます。

async/awaitでのエラーハンドリング

async/awaitを使う場合、エラーハンドリングにはtry...catchを使用します。これにより、同期処理と同じようにエラーハンドリングができ、コードがよりシンプルになります。

async function fetchData() {
  try {
    const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error("エラーが発生しました:", error);
  } finally {
    console.log("非同期処理が終了しました");
  }
}

fetchData();

この例では、try...catch構文を使って、awaitで待機している処理のエラーを捕捉しています。finallyも併用することで、クリーンアップ処理を行うこともできます。

Promiseのエラーハンドリングの重要性

Promiseを使用する際に、エラーハンドリングは非常に重要です。適切なエラーハンドリングを行わないと、アプリケーションが予期しない動作をする可能性があり、ユーザーエクスペリエンスを損ねることがあります。また、エラーの原因を迅速に特定し、解決するためにも、エラーハンドリングは欠かせません。

Promiseを使ったエラーハンドリングを理解し、catch()finally()try...catchを活用することで、より堅牢で信頼性の高いアプリケーションを構築することができます。

Promiseのネスト問題と解決策

Promiseを使った非同期処理では、複数のPromiseを連続して実行する場合にネスト構造が発生することがあります。このネストは、可読性を下げ、メンテナンスが難しくなる原因となります。特に複数の非同期処理が依存関係を持っている場合、Promiseのネストが深くなり、コードが煩雑になります。しかし、async/awaitを使うことで、Promiseのネスト問題を簡潔に解決できます。

Promiseのネスト問題

Promiseを連続して使うと、ネスト構造が発生しやすくなります。以下は、典型的なPromiseのネスト問題を示す例です。

function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("データを取得しました");
    }, 1000);
  });
}

function processData(data: string) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(`${data} を処理しました`);
    }, 1000);
  });
}

function displayData(result: string) {
  console.log(result);
}

// ネストされたPromiseチェーン
fetchData()
  .then((data) => {
    processData(data)
      .then((processedData) => {
        displayData(processedData);
      })
      .catch((error) => console.error(error));
  })
  .catch((error) => console.error(error));

このコードでは、fetchData()の結果を使ってprocessData()を実行し、最終的にdisplayData()で結果を表示しています。しかし、Promiseがネストされているため、コードが読みづらくなっています。このようなネストが深くなると、「Promiseのピラミッド」と呼ばれる非効率な構造が生まれ、保守性が低下します。

async/awaitによる解決策

このネスト構造を解決するために、async/awaitを使うと、非同期処理を同期処理のように記述できます。これにより、ネストがなくなり、コードの可読性と保守性が大幅に向上します。

async function executeAsyncProcesses() {
  try {
    const data = await fetchData();
    const processedData = await processData(data);
    displayData(processedData);
  } catch (error) {
    console.error("エラー:", error);
  }
}

executeAsyncProcesses();

この例では、async関数を使って非同期処理を連続的に行っています。awaitを使うことで、各Promiseが解決されるまで待機し、その後の処理を同期的に実行するかのように記述しています。この方法では、Promiseのネストが完全になくなり、コードがシンプルで直感的になります。

async/awaitの利点

  1. コードの可読性向上: async/awaitを使うことで、非同期処理のネストがなくなり、コードが直線的になり、可読性が大幅に向上します。
  2. エラーハンドリングの統一: try...catchを使って、一箇所でエラーハンドリングが可能になるため、複数のcatch()メソッドを使う必要がありません。
  3. デバッグが容易: 同期処理のようにコードが書けるため、デバッグが簡単になり、エラーの発生箇所を特定しやすくなります。

Promiseチェーンとasync/awaitの使い分け

  • Promiseチェーン: 簡単な非同期処理や、依存関係が少ない処理の場合に適しています。ネストが浅ければ、then()catch()を使ったPromiseチェーンも有効です。
  • async/await: 複数の非同期処理が連続的に行われる場合や、複雑な非同期処理が必要な場合に適しています。特にPromiseがネストする場面では、async/awaitを使う方が良いでしょう。

ネストのない非同期処理フローの構築

async/awaitを使うと、非同期処理を直線的に記述でき、Promiseのネストを避けることができます。これは、コードの可読性を保ちつつ、複雑な非同期処理を管理するための最も効果的な方法の一つです。Promiseのネスト問題を解決することで、保守性の高いコードを維持できるようになります。

非同期処理が多いアプリケーションでは、async/awaitを活用して、コードをシンプルにし、効率的な開発を目指しましょう。

Promiseとコールバックの違い

JavaScriptにおいて、非同期処理を管理するための方法として従来使われていたのがコールバックです。コールバックは、非同期処理が完了したときに呼び出される関数であり、Promiseが登場する以前に広く使われていました。しかし、コールバックには複雑なコードを引き起こしやすいという問題があり、Promiseがその問題を解決するために導入されました。本セクションでは、コールバックとPromiseの違いを解説し、それぞれのメリットとデメリットを比較します。

コールバックの仕組み

コールバックは、非同期処理が終了した後に、指定した関数を実行する仕組みです。例えば、データの取得が完了したときにその結果を処理する関数を指定します。

function fetchData(callback: (data: string) => void) {
  setTimeout(() => {
    callback("データを取得しました");
  }, 1000);
}

fetchData((data) => {
  console.log(data); // "データを取得しました" が表示される
});

この例では、fetchData関数にコールバックを渡し、そのコールバックが非同期処理完了後に実行されます。シンプルな非同期処理にはコールバックで十分ですが、複数の非同期処理を連続して行う場合、コールバックのネストが深くなり、コールバック地獄(Callback Hell)と呼ばれる可読性の低いコードが生まれます。

コールバック地獄の例

複数の非同期処理をコールバックで管理すると、次のようにネストが深くなり、コードが見づらくなることがあります。

function fetchData(callback: (data: string) => void) {
  setTimeout(() => {
    callback("データを取得しました");
  }, 1000);
}

function processData(data: string, callback: (processedData: string) => void) {
  setTimeout(() => {
    callback(`${data} を処理しました`);
  }, 1000);
}

fetchData((data) => {
  processData(data, (processedData) => {
    console.log(processedData); // "データを取得しました を処理しました" が表示される
  });
});

このように、コールバックを入れ子にして非同期処理を実行すると、ネストが深くなり、可読性が著しく低下します。これを解決するためにPromiseが導入されました。

Promiseの仕組み

Promiseは、非同期処理をオブジェクトとして扱い、処理の成功時(resolve)と失敗時(reject)を明確に管理します。Promiseを使うことで、コールバック地獄を避け、ネストのないシンプルなコードを書くことが可能です。

function fetchData(): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("データを取得しました");
    }, 1000);
  });
}

function processData(data: string): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(`${data} を処理しました`);
    }, 1000);
  });
}

fetchData()
  .then((data) => processData(data))
  .then((processedData) => {
    console.log(processedData); // "データを取得しました を処理しました" が表示される
  })
  .catch((error) => {
    console.error("エラー:", error);
  });

Promiseを使うことで、then()メソッドで非同期処理を順次つなげていくことができ、ネストを避けることができます。また、catch()メソッドを使えば、エラーハンドリングも一元化できます。

Promiseのメリット

  1. 可読性の向上: 非同期処理の結果をチェーン式に管理でき、ネストを回避できるため、コードがシンプルで可読性が高くなります。
  2. エラーハンドリングの一元化: 複数の非同期処理に対して、一つのcatch()でエラーハンドリングができるため、コード全体の管理が容易になります。
  3. 非同期処理の管理が簡単: Promise.all()Promise.race()などのメソッドを使えば、複数の非同期処理を効率的に管理できます。

コールバックのデメリットとPromiseの優位性

  • コールバック地獄: 複数の非同期処理をコールバックで行うと、ネストが深くなり、コードが非常に読みにくくなります。Promiseはこの問題を解決します。
  • エラー処理の複雑さ: コールバックを使ったエラーハンドリングは複雑になりがちですが、Promiseではエラーが自動的にcatch()に渡され、処理が一元化されます。

まとめ: コールバックとPromiseの使い分け

コールバックはシンプルな非同期処理に適していますが、複数の非同期処理が絡む場合はPromiseを使う方が効果的です。Promiseを利用することで、ネストのない、読みやすくメンテナンスしやすいコードを書くことができ、エラーハンドリングも簡単になります。非同期処理が複雑化する場面では、Promiseを活用して、より直感的で管理しやすいコードを実現しましょう。

実践的な演習問題

Promiseの基本的な使い方を理解したところで、さらに理解を深めるために、いくつかの実践的な演習問題を解いてみましょう。これらの問題は、Promiseのチェーン、async/await、エラーハンドリング、そして複数の非同期処理を扱う能力を鍛えるためのものです。

演習問題 1: データ取得と処理の連携

次のようなシナリオを考えます。サーバーからユーザーのデータを取得し、そのデータを加工してコンソールに表示するPromiseチェーンを作成してください。

  1. fetchUserData() という関数を作成し、サーバーからユーザー情報を取得します。この関数はPromiseを返すものとします。
  2. processUserData() という関数を作成し、取得したユーザーデータを加工します。この関数もPromiseを返します。
  3. Promiseチェーンを使って、ユーザーデータを取得・加工し、その結果をコンソールに表示してください。
function fetchUserData(): Promise<{ name: string; age: number }> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ name: "John", age: 30 });
    }, 1000);
  });
}

function processUserData(userData: { name: string; age: number }): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(`ユーザー名: ${userData.name}, 年齢: ${userData.age}`);
    }, 1000);
  });
}

// Promiseチェーンを使った実装
fetchUserData()
  .then((userData) => processUserData(userData))
  .then((processedData) => {
    console.log(processedData); // ユーザー名: John, 年齢: 30
  })
  .catch((error) => {
    console.error("エラー:", error);
  });

演習問題 2: async/awaitを使った非同期処理

上記の問題を、Promiseチェーンではなくasync/awaitを使って書き直してください。

async function fetchAndProcessUserData() {
  try {
    const userData = await fetchUserData();
    const processedData = await processUserData(userData);
    console.log(processedData); // ユーザー名: John, 年齢: 30
  } catch (error) {
    console.error("エラー:", error);
  }
}

fetchAndProcessUserData();

演習問題 3: 複数のAPIリクエストを同時に処理

Promise.all()を使って、複数のAPIリクエストを同時に処理してください。以下のような関数が2つあります。それぞれの関数から取得したデータをまとめて表示するコードを実装してください。

  1. fetchUserDetails() という関数はユーザーの詳細情報を取得します。
  2. fetchUserPosts() という関数は、そのユーザーが書いたブログ投稿を取得します。
  3. これら2つの非同期処理を同時に実行し、結果を一括で処理してください。
function fetchUserDetails(): Promise<{ name: string; email: string }> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ name: "John Doe", email: "john@example.com" });
    }, 1500);
  });
}

function fetchUserPosts(): Promise<string[]> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(["Post 1", "Post 2", "Post 3"]);
    }, 1000);
  });
}

// Promise.allを使った実装
Promise.all([fetchUserDetails(), fetchUserPosts()])
  .then(([userDetails, userPosts]) => {
    console.log("ユーザー詳細:", userDetails);
    console.log("ユーザーの投稿:", userPosts);
  })
  .catch((error) => {
    console.error("エラー:", error);
  });

演習問題 4: エラーハンドリングの実装

非同期処理のどこかでエラーが発生した場合、それを適切にハンドリングしてください。次のコードでは、fetchUserData()でエラーが発生する可能性があります。このエラーをcatch()メソッドで処理し、async/awaitのバージョンでもエラーハンドリングを実装してください。

function fetchUserDataWithError(): Promise<{ name: string; age: number }> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("ユーザーデータの取得に失敗しました");
    }, 1000);
  });
}

// Promiseチェーンでのエラーハンドリング
fetchUserDataWithError()
  .then((data) => console.log(data))
  .catch((error) => {
    console.error("Promiseチェーンエラー:", error);
  });

// async/awaitでのエラーハンドリング
async function handleUserDataWithError() {
  try {
    const userData = await fetchUserDataWithError();
    console.log(userData);
  } catch (error) {
    console.error("async/awaitエラー:", error);
  }
}

handleUserDataWithError();

演習問題 5: Promise.race()を使ったタイムアウト処理

APIリクエストが指定した時間内に完了しない場合にタイムアウトさせるコードをPromise.race()を使って実装してください。以下のコードを参考にして、APIリクエストかタイムアウトのどちらか早い方を優先する処理を作成します。

function apiRequest(): Promise<string> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("APIリクエストが完了しました");
    }, 2000);
  });
}

function timeout(ms: number): Promise<string> {
  return new Promise((_, reject) => {
    setTimeout(() => {
      reject("タイムアウト");
    }, ms);
  });
}

// Promise.raceを使ったタイムアウト処理
Promise.race([apiRequest(), timeout(1500)])
  .then((result) => {
    console.log(result);
  })
  .catch((error) => {
    console.error(error); // "タイムアウト" が表示される
  });

まとめ

これらの演習問題を通じて、Promiseやasync/awaitを使った非同期処理の理解をさらに深めることができます。Promiseチェーン、複数の非同期処理の並行実行、エラーハンドリングなど、実践的なシナリオを解決するスキルを習得しましょう。

まとめ

本記事では、TypeScriptにおけるPromiseの基本的な使い方から、then()catch()Promise.all()Promise.race()といったメソッドの活用、さらにはasync/awaitを使った非同期処理のシンプルな書き方までを詳しく解説しました。Promiseを使えば、非同期処理の可読性やエラーハンドリングが向上し、複雑な処理でも管理が容易になります。これらの知識を活用して、効率的かつ直感的に非同期処理を扱えるようになりましょう。

コメント

コメントする

目次