TypeScriptの開発において、非同期処理は不可欠な要素です。非同期処理を適切に設計し実装することで、アプリケーションのパフォーマンスやユーザー体験を向上させることができます。しかし、並列実行と逐次実行の違いや、型安全な設計を取り入れることで、より信頼性の高いコードを実現するには深い理解が必要です。本記事では、TypeScriptでの非同期処理の並列実行と逐次実行に焦点を当て、その違いや設計方法、さらには型安全な方法での実装について詳しく解説します。
非同期処理とは何か
非同期処理とは、時間のかかる操作(例えば、APIリクエストやファイルの読み書き)が完了するまで、他の処理をブロックせずに進行させる手法です。JavaScriptやTypeScriptでは、イベントループを用いることで、単一のスレッドでこのような非同期処理を効率的に実現しています。
JavaScript/TypeScriptにおける非同期処理の特徴
JavaScriptやTypeScriptは、シングルスレッドで動作するため、非同期処理は特に重要です。これにより、時間のかかる処理が終了するのを待たずに、他のタスクを並行して実行することが可能です。通常、非同期処理には以下の3つの代表的な手法が用いられます:
- コールバック関数
- Promise
- async/await
これらの手法により、効率的な非同期処理の実装が可能になり、特にasync/awaitを使うことで、同期的なコードと同じように記述できるのが特徴です。
並列実行と逐次実行の違い
非同期処理における並列実行と逐次実行は、タスクをどのように処理するかに関する重要な設計概念です。両者の違いを理解することで、アプリケーションのパフォーマンスやコードの可読性を向上させることができます。
並列実行とは
並列実行とは、複数の非同期タスクを同時に実行し、それぞれが完了するのを待たずに次のタスクを進行させる方法です。これにより、時間のかかる処理が効率よく進行するため、全体の処理時間を短縮できます。TypeScriptでは、Promise.all
やPromise.race
を用いることで、複数の非同期タスクを並列に実行できます。
並列実行のメリット
- 複数のタスクを同時に実行することで、総処理時間が短くなる。
- 非同期処理の際にパフォーマンスを最適化できる。
逐次実行とは
逐次実行とは、タスクを一つずつ順番に実行し、前のタスクが完了してから次のタスクを開始する方法です。async/await
を使うことで、非同期タスクを逐次的に実行できます。この方法は、各処理が依存関係を持つ場合や、順序が重要な場面で有効です。
逐次実行のメリット
- 各タスクが完了した順に実行されるため、予測可能な動作を保証。
- 複雑な依存関係を持つ処理で有効。
これらの実行方法を適切に使い分けることで、より効率的な非同期処理を実現できます。
Promise.allとPromise.raceの利用法
TypeScriptで非同期タスクを並列に実行する際に、Promise.all
とPromise.race
は非常に便利なメソッドです。それぞれの特徴を理解し、適切な場面で使うことで、効率的な並列処理を実現できます。
Promise.allとは
Promise.all
は、複数のPromiseを一度に受け取り、全てのPromiseが解決(成功)するまで待機し、それらの結果を配列として返します。すべてのタスクが完了するまで結果を待つ必要がある場合に使用します。
const task1 = new Promise((resolve) => setTimeout(() => resolve('Task 1 complete'), 1000));
const task2 = new Promise((resolve) => setTimeout(() => resolve('Task 2 complete'), 2000));
Promise.all([task1, task2]).then((results) => {
console.log(results); // ['Task 1 complete', 'Task 2 complete']
});
Promise.allのメリット
- 全ての非同期処理が完了するのを待ってから結果を得られるため、データの一貫性が保たれる。
- 複数の非同期処理を同時に実行することで、総処理時間を短縮できる。
Promise.allのデメリット
- 一つでもPromiseが拒否(失敗)されると、全体が失敗として扱われる。
Promise.raceとは
Promise.race
は、複数のPromiseのうち最初に解決または拒否されたものの結果を返します。一番早く完了する非同期処理の結果が必要な場合に使用します。
const task1 = new Promise((resolve) => setTimeout(() => resolve('Task 1 complete'), 1000));
const task2 = new Promise((resolve) => setTimeout(() => resolve('Task 2 complete'), 2000));
Promise.race([task1, task2]).then((result) => {
console.log(result); // 'Task 1 complete'
});
Promise.raceのメリット
- 最初に完了したPromiseの結果をすぐに取得できるため、素早いレスポンスが求められる場面で有効。
Promise.raceのデメリット
- 最初に解決または拒否された結果のみが返されるため、残りのタスクの完了は確認できない。
Promise.all
とPromise.race
は、タスクの性質に応じて使い分けることが重要です。それぞれのメソッドを活用することで、TypeScriptでの非同期処理が効率的かつ柔軟に実現できます。
async/awaitを使った逐次実行の実装
TypeScriptで非同期処理をより直感的に記述できるのが、async/await
構文です。この構文を使うことで、非同期処理を同期的に見える形で記述でき、可読性が向上します。また、async/await
は逐次実行を簡単に実現する手段として非常に有効です。
async/awaitの基本
async
関数は、常にPromiseを返します。関数内でawait
を使うと、そのPromiseが解決されるまで処理を一時停止し、解決後に続きの処理が実行されます。この動作により、非同期タスクを順序立てて実行することができます。
async function fetchData() {
const response1 = await fetch('https://api.example.com/data1');
const data1 = await response1.json();
const response2 = await fetch('https://api.example.com/data2');
const data2 = await response2.json();
console.log(data1, data2);
}
fetchData();
上記の例では、await
を使うことで、fetch
によるAPIリクエストが順番に実行され、結果が逐次的に処理されます。
async/awaitのメリット
- 非同期コードを同期的なコードのように記述でき、可読性が向上する。
then
やcatch
をチェーンするPromiseよりも、エラーハンドリングやデバッグが容易になる。- タスクの順序が重要な場合、逐次的にタスクを処理できる。
逐次実行の例
逐次実行が必要なケースでは、各タスクが前のタスクに依存している場合や、処理の順番が重要な場合が典型的です。
async function processSequentially() {
console.log('Starting Task 1...');
const result1 = await task1(); // Task 1の完了を待つ
console.log(result1);
console.log('Starting Task 2...');
const result2 = await task2(); // Task 2の完了を待つ
console.log(result2);
}
processSequentially();
ここでは、task1
が完了してからtask2
を実行しており、逐次的な実行が確保されています。
逐次実行の利点
- 各タスクが確実に完了してから次に進むため、処理の順序が守られる。
- データの依存関係がある場合に、順次実行は非常に有効。
async/awaitでの逐次実行のデメリット
- タスクが並列に実行されないため、複数の非同期処理を同時に行う場合に比べて処理速度が遅くなることがあります。
適切に逐次実行を選択することで、データの依存関係があるシステムや、順序が重要な処理を型安全に実装できます。
型安全な非同期処理設計の重要性
TypeScriptの強力な特徴の一つは、型システムを活用して安全性を確保できることです。特に非同期処理において型安全な設計を行うことで、バグを未然に防ぎ、保守性の高いコードを実現できます。非同期処理は、外部APIからのデータ取得や、時間のかかる処理など、エラーが発生しやすい場面が多いため、型の整合性を保つことが重要です。
型安全な設計の利点
TypeScriptで非同期処理を行う際に型安全な設計を取り入れると、以下の利点が得られます:
1. エラー防止
非同期処理において、Promiseが返す値や関数が期待する引数の型が明確でないと、予期しないエラーが発生する可能性があります。TypeScriptの型チェックを活用することで、型の不一致によるエラーをコンパイル時に防ぐことができます。
async function fetchData(): Promise<string> {
const response = await fetch('https://api.example.com/data');
return await response.json(); // この返り値の型がstringであることを保証
}
このように、関数の戻り値に型を指定することで、返されるデータが想定された型であることを保証します。
2. コードの可読性と保守性の向上
型定義があることで、関数が返すデータの型や非同期処理の流れが明確になります。これにより、コードの可読性が向上し、他の開発者や未来の自分がそのコードを保守する際にも理解しやすくなります。
3. 自動補完による開発効率の向上
型情報を提供することで、IDE(統合開発環境)が自動補完機能を最大限に活用でき、開発のスピードと精度が向上します。特に、APIから返されるデータの型を正しく定義することで、非同期処理中の各ステップでどのプロパティが利用可能かがすぐにわかります。
型安全性が欠如するリスク
非同期処理で型安全な設計を怠ると、次のような問題が発生しやすくなります:
- APIの仕様変更や、想定外のデータ形式が原因で実行時エラーが発生。
- 大規模なプロジェクトで型の不一致が原因でデバッグに時間を費やす。
- 保守時にデータの流れや型の整合性がわからず、コードの変更が難しくなる。
型安全な設計を採用することで、これらのリスクを軽減し、非同期処理をより安全かつ効率的に管理できるようになります。
TypeScriptで非同期処理を型安全にする方法
TypeScriptでは、非同期処理を型安全に設計するためのさまざまな方法が提供されています。これにより、非同期タスクで扱うデータの型が保証され、エラーを未然に防ぐことができます。ここでは、Promiseやasync/awaitを使った型安全な非同期処理の実装方法を具体的に紹介します。
Promiseの型定義
Promise
は、将来的に値を返す非同期処理を表すオブジェクトです。TypeScriptでは、Promise
が解決する際に返される値の型を指定することができます。これにより、非同期タスクの結果がどの型になるかが明確に定義され、予期しない型のエラーを防ぐことができます。
function fetchUser(): Promise<{ name: string; age: number }> {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ name: 'John', age: 30 });
}, 1000);
});
}
上記の例では、fetchUser
関数がPromiseを返し、そのPromiseはname
とage
を持つオブジェクトを返すことが型で定義されています。この型定義により、返されるデータが必ずオブジェクト形式で、特定のプロパティを持つことが保証されます。
async/awaitでの型定義
async
関数でも、戻り値の型をPromiseとして指定できます。async
関数自体がPromiseを返すため、非同期処理の結果に適切な型を与えることが重要です。
async function getUserInfo(): Promise<{ name: string; age: number }> {
const response = await fetch('https://api.example.com/user');
const data = await response.json();
return { name: data.name, age: data.age };
}
この例では、APIから取得したデータに対してname
とage
が必ず存在し、その型が適切に定義されています。これにより、getUserInfo
を呼び出す際に、返されるオブジェクトが正しい型を持つことが保証されます。
非同期関数におけるジェネリクスの活用
ジェネリクスを使うことで、非同期処理においてさらに柔軟で型安全な設計が可能になります。ジェネリクスを活用すれば、関数の呼び出し時に型を指定でき、再利用性の高いコードを実現できます。
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
const data: T = await response.json();
return data;
}
// 使用例
interface User {
name: string;
age: number;
}
async function main() {
const userData = await fetchData<User>('https://api.example.com/user');
console.log(userData.name, userData.age);
}
この例では、fetchData
関数にジェネリクスを使用して、任意の型T
のデータを取得することが可能です。これにより、非同期処理で返されるデータの型を柔軟に指定でき、異なるAPIエンドポイントからさまざまなデータを取得する場面で非常に有効です。
型安全なエラーハンドリング
非同期処理では、エラーが発生する可能性が常に存在します。そのため、エラーハンドリングにも型安全な設計を取り入れることが重要です。try/catch
ブロックを活用することで、非同期処理におけるエラーをキャッチし、適切な型で扱うことができます。
async function fetchWithErrorHandling(): Promise<string> {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data: { message: string } = await response.json();
return data.message;
} catch (error) {
return `Error: ${(error as Error).message}`;
}
}
この例では、エラーメッセージの型も定義しており、エラーが発生した場合でも型安全な方法でエラーハンドリングを行っています。
型安全な非同期処理の設計は、信頼性の高いアプリケーション開発に不可欠です。TypeScriptを活用することで、非同期タスクにおいてもエラーの少ない堅牢なコードを実現できます。
エラーハンドリングのベストプラクティス
非同期処理においてエラーハンドリングは非常に重要です。適切なエラーハンドリングを実装しないと、非同期タスクが失敗した際に予期しない挙動を引き起こす可能性があります。TypeScriptの強力な型システムを活用することで、非同期処理のエラーを型安全に処理でき、予測可能で信頼性の高いコードを記述できます。
Promiseのエラーハンドリング
Promise
を使用した非同期処理では、.catch()
メソッドを使ってエラーをキャッチします。以下の例では、APIリクエストに失敗した場合にエラーメッセージを返す仕組みを実装しています。
const fetchData = (): Promise<string> => {
return fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.catch(error => {
return `Error: ${error.message}`;
});
};
このように、Promise
チェーンの最後に.catch()
を追加することで、エラーが発生した際に適切なメッセージを表示することができます。
async/awaitとtry/catchによるエラーハンドリング
async/await
構文を使用した非同期処理では、try/catch
ブロックを使ってエラーをキャッチします。この方法は、同期処理のようにエラーハンドリングを記述できるため、可読性が高く、デバッグも容易です。
async function fetchData(): Promise<string> {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Failed to fetch data');
}
const data = await response.json();
return data.message;
} catch (error) {
return `Error: ${(error as Error).message}`;
}
}
上記の例では、try/catch
を使って非同期処理内のエラーをキャッチし、適切なエラーメッセージを返しています。async/await
構文におけるエラーハンドリングは、コードの流れが自然で理解しやすいのが特徴です。
エラーの再スローとロギング
エラーをキャッチした後にそのまま処理を進めるのではなく、場合によってはエラーを再度スロー(throw
)し、上位の関数にエラーを伝搬させることが重要です。エラーを再スローすることで、問題が発生した箇所を正確に特定でき、上位のロジックで適切な対応が可能になります。また、ログシステムにエラーを記録しておくこともベストプラクティスです。
async function fetchDataWithLogging(): Promise<string> {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Failed to fetch data');
}
const data = await response.json();
return data.message;
} catch (error) {
console.error('Error occurred:', error);
throw error; // エラーを再スロー
}
}
ここでは、エラーをキャッチしてログに記録した後、再スローして上位の処理にエラーを伝えています。これにより、非同期タスクのエラー情報を失うことなく適切に処理できます。
エラーハンドリングでの型安全の確保
非同期処理のエラーハンドリングにおいても、型安全を確保することが重要です。エラーメッセージが特定の形式であることを保証するため、Error
型を明示的に指定することで、開発中に型の不整合を防ぐことができます。
async function processData(): Promise<string> {
try {
const data = await fetchData();
return data;
} catch (error) {
if (error instanceof Error) {
return `Error: ${error.message}`;
} else {
return 'Unknown error occurred';
}
}
}
このように、Error
型のインスタンスであるかどうかを確認することで、エラー処理の一貫性を保ち、型安全なエラーハンドリングが可能になります。
エラーハンドリングのベストプラクティスのまとめ
Promise
では.catch()
、async/await
ではtry/catch
を使ってエラーをキャッチする。- 可能な場合はエラーを再スローし、上位の関数で適切に処理する。
- エラーは常にログに記録し、後から分析できるようにする。
- 型安全なエラーハンドリングを行い、予期しない型のエラーを避ける。
これらのベストプラクティスを守ることで、TypeScriptにおける非同期処理のエラーハンドリングを効率的かつ安全に実装できます。
非同期処理のパフォーマンス最適化
非同期処理の実装において、パフォーマンス最適化は非常に重要です。TypeScriptでは、非同期タスクを適切に設計し実装することで、処理速度を向上させ、リソースの無駄を最小限に抑えることができます。特に、並列実行と逐次実行を使い分け、必要なリソースを効率的に利用することがパフォーマンス向上の鍵です。
並列実行と逐次実行のパフォーマンスの違い
非同期処理における並列実行と逐次実行の選択は、パフォーマンスに直接影響を与えます。
並列実行によるパフォーマンス向上
複数の非同期タスクを同時に実行する並列処理は、処理時間を大幅に短縮することが可能です。例えば、APIリクエストやファイル操作など、タスクが互いに依存しない場合、並列に実行することで全体の処理時間を減らすことができます。
const task1 = new Promise((resolve) => setTimeout(() => resolve('Task 1 complete'), 1000));
const task2 = new Promise((resolve) => setTimeout(() => resolve('Task 2 complete'), 2000));
Promise.all([task1, task2]).then((results) => {
console.log(results); // ['Task 1 complete', 'Task 2 complete'] 2秒で両方完了
});
この例では、Promise.all
を使用することで2つのタスクを並列実行し、タスク全体の実行時間を短縮しています。両方のタスクが完了するまでの時間は最長のタスク(2秒)のみです。
逐次実行のパフォーマンス特性
逐次実行は、各タスクが順番に実行されるため、個々のタスクが終了するまで次のタスクに進むことができません。依存関係のある処理や、タスク間でデータのやり取りが必要な場合には有効ですが、全体の処理時間が長くなる可能性があります。
async function processSequentially() {
const result1 = await task1();
const result2 = await task2();
console.log(result1, result2); // 3秒後に結果が表示
}
ここでは、タスクが順番に実行されるため、全体の処理時間は合計で3秒となります。逐次実行は、パフォーマンスの観点から見ると効率的ではない場合があります。
非同期処理のパフォーマンスボトルネック
非同期処理の最適化では、次のようなボトルネックに注意する必要があります:
- API呼び出しのオーバーヘッド:大量のAPIリクエストを同時に送信すると、サーバーの負荷が増大し、レスポンスタイムが遅くなる可能性があります。適切なタイミングでリクエストを送信し、キャッシングなどを活用してサーバー負荷を軽減することが重要です。
- リソースの競合:同時に複数のタスクが実行されることで、メモリやネットワーク帯域などのリソースが競合し、パフォーマンスが低下することがあります。この場合、逐次実行やリソース管理を組み合わせて対処します。
パフォーマンス最適化の具体的な方法
1. 適切な並列制御
並列処理は効率的ですが、同時に処理できるタスク数を制限することで、システムリソースの無駄遣いや負荷の増加を防げます。Promise.allSettled
やPromise.all
を使いつつ、制御された並列処理を行うことが推奨されます。
async function limitedParallelTasks(tasks: (() => Promise<any>)[], limit: number) {
const results = [];
const executing: Promise<any>[] = [];
for (const task of tasks) {
const p = task().then(result => {
executing.splice(executing.indexOf(p), 1);
return result;
});
results.push(p);
executing.push(p);
if (executing.length >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
この例では、並列実行するタスク数に制限を設け、過剰な負荷を避けながら効率的に非同期処理を実行しています。
2. レイジーローディングの活用
非同期処理では、必要なタイミングでのみリソースを取得する「レイジーローディング」も重要な最適化手法です。不要なデータを事前に取得するのではなく、必要なときに非同期でロードすることで、初期の処理負荷を軽減できます。
3. データのキャッシング
同じデータを複数回取得する必要がある場合、キャッシングを利用してAPIリクエストの回数を減らすことができます。これにより、処理速度を向上させ、サーバーへの負荷を軽減できます。
非同期処理のパフォーマンス最適化のまとめ
- 並列実行を効果的に活用し、タスクの総処理時間を短縮する。
- 適切な並列制御やリソース管理を行い、ボトルネックを避ける。
- レイジーローディングやキャッシングなどを駆使して、リソースの無駄遣いを減らす。
これらの最適化手法を活用することで、TypeScriptでの非同期処理を効率的に実行し、アプリケーションのパフォーマンスを最大限に引き出すことが可能になります。
非同期処理設計の具体例
ここでは、TypeScriptを使った非同期処理の具体的な設計例を紹介します。これにより、並列処理と逐次処理、エラーハンドリング、型安全な設計を実際のシナリオに落とし込む方法を理解できます。今回は、複数のAPIからデータを取得し、これらのデータを組み合わせて処理するケースを見ていきます。
具体例:複数のAPIからデータを並列に取得し、統合する
例えば、ユーザー情報とその投稿情報を異なるAPIから取得し、それらを統合して一つのオブジェクトとして返すシナリオを考えます。
1. 並列実行を用いたデータ取得
Promise.all
を使って、複数のAPIリクエストを並列で実行し、効率的にデータを取得します。並列処理により、全体の処理時間を短縮できます。
interface User {
id: number;
name: string;
}
interface Post {
id: number;
title: string;
}
async function fetchUser(userId: number): Promise<User> {
const response = await fetch(`https://api.example.com/users/${userId}`);
return await response.json();
}
async function fetchPosts(userId: number): Promise<Post[]> {
const response = await fetch(`https://api.example.com/users/${userId}/posts`);
return await response.json();
}
async function getUserAndPosts(userId: number) {
try {
const [user, posts] = await Promise.all([fetchUser(userId), fetchPosts(userId)]);
return { user, posts };
} catch (error) {
throw new Error(`Failed to fetch data: ${(error as Error).message}`);
}
}
getUserAndPosts(1).then(data => console.log(data));
この例では、Promise.all
を使ってユーザー情報とその投稿を並列に取得しています。全ての非同期タスクが完了すると、それらの結果が一つのオブジェクトにまとめられます。この方法で、複数のAPIリクエストを効率的に処理でき、全体の待機時間を短縮できます。
2. 逐次実行を用いたデータ処理
続いて、あるタスクが完了した後に次のタスクを実行する逐次処理の例を見ていきます。ここでは、APIリクエストの結果に依存して次の処理を行うケースを想定します。
async function getUserWithPosts(userId: number): Promise<void> {
try {
const user = await fetchUser(userId); // ユーザー情報を取得
console.log(`User: ${user.name}`);
const posts = await fetchPosts(userId); // ユーザーに基づき投稿情報を取得
console.log(`Posts: ${posts.length} posts found`);
// データを使ってさらに処理
posts.forEach(post => console.log(post.title));
} catch (error) {
console.error(`Error: ${(error as Error).message}`);
}
}
getUserWithPosts(1);
この例では、ユーザー情報を取得した後に、そのユーザーに関連する投稿データを取得するため、逐次処理を使用しています。ここで、非同期タスク間に依存関係があるため、逐次実行が適切です。
3. 型安全なエラーハンドリング
エラーが発生した場合には、型安全な方法でエラーハンドリングを行うことが重要です。エラーハンドリングを適切に行うことで、実行時エラーの影響を最小限に抑えることができます。
async function fetchDataWithErrorHandling<T>(url: string): Promise<T> {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
return await response.json();
} catch (error) {
console.error('Error fetching data:', error);
throw error; // 上位関数で再度エラーハンドリング
}
}
async function getUserData(userId: number) {
try {
const user = await fetchDataWithErrorHandling<User>(`https://api.example.com/users/${userId}`);
const posts = await fetchDataWithErrorHandling<Post[]>(`https://api.example.com/users/${userId}/posts`);
return { user, posts };
} catch (error) {
console.error('Error in getUserData:', error);
}
}
getUserData(1);
このコードでは、fetchDataWithErrorHandling
という汎用的なエラーハンドリング関数を使い、型安全にエラーを処理しています。データの取得に失敗した場合、上位関数でもエラーをキャッチし、適切な対策を講じることができます。
4. 最適化された非同期処理
並列処理と逐次処理を適切に組み合わせることで、効率的なパフォーマンスを発揮できます。例えば、依存関係のない処理は並列に実行し、依存関係のあるタスクは逐次的に処理します。これにより、全体の処理速度を最適化できます。
async function fetchAllData(userId: number) {
try {
// 並列処理でユーザーと投稿情報を同時に取得
const [user, posts] = await Promise.all([fetchUser(userId), fetchPosts(userId)]);
// 逐次的にさらに処理
posts.forEach(post => {
console.log(`Post title: ${post.title}`);
});
return { user, posts };
} catch (error) {
console.error(`Failed to fetch data: ${(error as Error).message}`);
}
}
fetchAllData(1);
この例では、並列実行と逐次実行を組み合わせて、パフォーマンスを向上させつつ効率的にデータを処理しています。
まとめ
TypeScriptでの非同期処理は、並列実行と逐次実行を適切に使い分け、エラーハンドリングを行うことで、効率的かつ信頼性の高いアプリケーションを構築できます。具体例を通じて、実際の設計方法を理解し、最適な非同期処理の実装に役立ててください。
演習問題
以下の演習問題を通じて、TypeScriptにおける非同期処理の知識を実践的に深めてみましょう。並列実行と逐次実行、エラーハンドリング、型安全な設計を活用したコードを書いてみてください。
演習問題 1: 並列実行の実装
複数のAPIエンドポイントからデータを並列で取得し、取得したデータをまとめて出力するプログラムを作成してください。以下のエンドポイントを利用してください:
https://api.example.com/resource1
https://api.example.com/resource2
https://api.example.com/resource3
- これら3つのリソースを並列で取得し、取得結果をコンソールに出力するコードを書いてください。
- Promise.allを使用して、全てのリソースを効率的に取得しましょう。
演習問題 2: 逐次実行の実装
逐次実行が必要な場合を想定し、以下の要件を満たすコードを書いてください:
- 最初に、
https://api.example.com/user
からユーザー情報を取得する。 - 次に、取得したユーザーのIDを使用して、そのユーザーの投稿情報を
https://api.example.com/user/{id}/posts
から取得する。 - 逐次的にAPIリクエストを実行し、最終的に取得したデータをコンソールに出力してください。
演習問題 3: エラーハンドリング
APIリクエスト中にエラーが発生した場合のエラーハンドリングを実装してください。次の要件に従ってコードを書いてみましょう:
- APIリクエストが失敗した場合、エラーメッセージを表示する。
- ネットワークエラーやAPIレスポンスが異常な場合、
try/catch
でエラーをキャッチして、エラーメッセージをコンソールに表示する。 - キャッチしたエラーは再スローし、上位で別のエラーハンドリングも行ってください。
演習問題 4: 型安全な非同期処理
非同期処理において型安全を維持するため、次の要件を満たすコードを実装してください:
- APIから取得するデータの型をインターフェースで定義し、それに基づいてPromiseの戻り値に型を付けてください。
- ジェネリックを使用して、異なるAPIリクエストに対しても型安全なデータ取得関数を実装してください。
これらの演習問題に取り組むことで、非同期処理の基本的な実装方法から、エラーハンドリングや型安全な設計まで、TypeScriptの非同期処理に関する理解が深まるはずです。
まとめ
本記事では、TypeScriptにおける非同期処理の並列実行と逐次実行について、型安全な設計方法と具体的な実装例を紹介しました。並列処理と逐次処理を正しく使い分け、Promiseやasync/awaitを活用することで、効率的かつメンテナンスしやすい非同期処理が可能になります。また、エラーハンドリングや型安全性を確保することで、より信頼性の高いコードを実現できることも確認しました。これらの知識を活かし、TypeScriptでの非同期処理をより効果的に設計・実装しましょう。
コメント