初心者がTypeScriptで学ぶasync/await
はじめに
現代のWeb開発において、非同期処理は避けて通れない重要な概念です。APIからのデータ取得、ファイルの読み書き、タイマー処理など、時間がかかる操作を行う際に、アプリケーションの応答性を維持するために非同期処理が必要となります。
TypeScriptにおける非同期処理の実装方法はいくつかありますが、中でもasync/awaitは読みやすく、保守性の高いコードを書くための強力な機能です。この記事では、非同期処理の基本からTypeScriptにおけるasync/awaitの使い方、そして実践的なパターンまでを初心者向けに解説します。
この記事を読むことで、次のことが理解できます!
- 非同期処理とは何か、そしてなぜ必要なのか
- Promiseの基本概念と使い方
-
async/awaitの構文と動作原理 - 効果的なエラーハンドリング
- 実践的なコード例
非同期処理とは何か
JavaScriptは基本的にシングルスレッドで動作します。つまり、一度に1つのタスクしか実行できません。しかし、データの取得やファイルの読み込みなど、時間のかかる操作を行う際に、処理が完了するまで他の操作をブロックしてしまうと、ユーザー体験が著しく低下します。
非同期処理を使うと、時間のかかる操作を「バックグラウンド」で実行しながら、メインのプログラムは他の処理を続けることができます。処理が完了したら、その結果を受け取って次の処理を行うという仕組みです。
従来のコールバックパターン
JavaScriptでは伝統的に、非同期処理はコールバック関数を使って実装されてきました:
fuction fetchData(callback: (error: Error | null, data: any) => void) {
setTimeout(() => {
if (/* 成功した場合 */) {
callback(null, "取得したデータ");
} else {
callback(new Error("データの取得に失敗しました"), null);
}
}, 1000);
}
// 使用例
fetchData((error, data) => {
if (error) {
console.error("エラーが発生しました:", error);
return;
}
console.log("データ:", data);
// ネストされたコールバック(コールバックヘル)
fetchData((error, data2) => {
if (error) {
console.error("エラーが発生しました:", error);
return;
}
console.log("データ2:", data2);
// さらにネスト...
});
});
このパターンの問題点は、複数の非同期処理を連鎖させる必要がある場合に「コールバックヘル」と呼ばれる深くネストされた構造になりがちなことです。コードの可読性と保守性が低下します。
Promiseの基本
コールバックヘルの問題を解決するために導入されたのがPromiseです。Promiseは非同期処理の最終的な完了(または失敗)とその結果の値を表すオブジェクトです。
Promiseの作成と使用
TypeScriptでPromiseを作成するには、Promiseコンストラクタを使用します:
function fetchData(): Promise<string> {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 非同期処理
const success = true; // 成功か失敗かのフラグ
if (success) {
resolve("取得したデータ"); // 成功時
} else {
reject(new Error("データの取得に失敗しました")); // 失敗時
}
}, 1000);
});
}
// 使用例
fetchData()
.then(data => {
console.log("データ:", data);
return fetchData(); // 別のPromiseを返す
})
.then(data2 => {
console.log("データ2:", data2);
})
.catch(error => {
console.error("エラーが発生しました:", error);
});
Promiseを使うことで、コールバックヘルの問題は改善されますが、.then()による連鎖が長くなると、まだ可読性に課題が残ります。
async/awaitの基本
async/awaitはPromiseをさらに直感的に扱うことができます。この構文を使うと、非同期コードがあたかも同期コードのように見え、理解しやすくなります。
async/await基本構文
async function fetchDataAsync(): Promise<string> {
try {
const data = await feathData(); // Promiseが解決されるまで待機
console.log("データ:", data);
const data2 = await feathData(); // 次のPromiseが解決されるまで待機
console.log("データ2:", data2);
return "すべての処理が完了しました";
} catch (error) {
console.error("エラーが発生しました:", error);
throw error; // エラーを再スロー
}
}
// 使用例
fetchDataAsync()
.then(result => console.log(result))
.catch(error => console.error("キャッチされたエラー:", error));
この例では、async関数内でawaitキーワードを使用して、Promiseが解決されるまで処理を一時停止しています。処理が再開されると、Promiseの結果が変数に代入されます。エラーハンドリングには通常のtry/catchブロックを使用できます。
asyncとawaitの動作原理
asyncとawaitがどのように動作するのか、基本的な仕組みを理解しましょう:
-
asyncキーワードを関数の前につけると、その関数は常にPromiseを返します。 -
Promiseでない値を返した場合、自動的にPromise.resolveでラップされます。 - 例外がスローされた場合、自動的にPromise.rejectでラップされます。
-
awaitキーワードはPromiseが解決されるまで関数の実行を一時停止します。 -
Promiseが解決されると、その結果がawait式の戻り値になります。 -
Promiseが拒否された場合、例外がスローされ、try/catchブロックでキャッチできます。
typescript// これは...
async function example() {
return "結果";
}
// ...これと同等です
function example() {
return Promise.resolve("結果");
}
実践的なasync/await
ここからは、TypeScriptでasync/awaitを使った実践的な例を見ていきましょう。
基本的なHTTPリクエスト
fetch APIを使った基本的なHTTPリクエストの例です:
typescriptinterface User {
id: number;
name: string;
email: string;
}
async function getUser(id: number): Promise<User> {
try {
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const user: User = await response.json();
return user;
} catch (error) {
console.error("ユーザー取得中にエラーが発生しました:", error);
throw error;
}
}
// 使用例
async function displayUser(id: number): Promise<void> {
try {
const user = await getUser(id);
console.log(`ユーザー: ${user.name} (${user.email})`);
} catch (error) {
console.error("ユーザー表示中にエラーが発生しました:", error);
}
}
displayUser(1);
この例では、fetch APIを使用してユーザーデータを取得し、結果を処理しています。async/awaitにより、コードが同期的に見えるため理解しやすくなっています。
エラーハンドリング
async/awaitを使用する際の効果的なエラーハンドリング戦略を見ていきましょう。
try/catchによるエラーハンドリング
最も一般的なアプローチは、try/catchブロックを使用することです:
typescriptasync function fetchData(): Promise<string> {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('データ取得中にエラーが発生しました:', error);
throw error; // エラーを再スロー
}
}
エラーの伝播
エラーを呼び出し元に伝播させることで、エラー処理を集中化できます:
typescriptasync function fetchData(): Promise<string> {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
async function processData(): Promise<void> {
try {
const data = await fetchData();
console.log(data);
} catch (error) {
// fetchData()からスローされたエラーをここでキャッチ
console.error('データ処理中にエラーが発生しました:', error);
}
}
エラーハンドリングの混合アプローチ
Promise構文とasync/await構文を組み合わせたエラーハンドリングも可能です:
typescriptasync function fetchData(): Promise<string> {
// エラーをスローする可能性がある
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
function handleError(error: any): void {
console.error('エラーハンドラー:', error);
}
async function processData(): Promise<void> {
// catch()を使用してエラーをハンドリング
const data = await fetchData().catch(handleError);
if (data) {
console.log(data);
}
}
この方法では、await式でPromiseの結果を取得しながらも、.catch()を使ってエラーを処理しています。
async/awaitのベストプラクティス
TypeScriptでasync/awaitを使用する際のベストプラクティスをいくつか紹介します:
- 適切なエラーハンドリングを常に実装する:
- 非同期処理では、エラーが発生する可能性が高いため、
try/catchブロックを使用して適切に処理しましょう。
- 不必要なawaitを避ける:
-
awaitの使いすぎはパフォーマンスのボトルネックとなる可能性があります。複数の独立した非同期処理がある場合は、Promise.all()を使用しましょう。
- 型情報を明示する:
- TypeScriptの型システムを活用して、
Promiseの戻り値の型を明示しましょう。これにより、コードの可読性と安全性が向上します。
typescriptasync function fetchUser(): Promise<User> {
// ...
}
- 非同期操作の結果をキャッシュする:
- 同じ非同期操作を繰り返し行う場合は、結果をキャッシュすることでパフォーマンスを向上させましょう。
- エラーハンドリングのメカニズムを統一する:
- アプリケーション全体で一貫したエラーハンドリングの方法を使用しましょう。
- トップレベルでの
awaitの使用に注意する:
- モジュールレベルでの
await(トップレベルawait)は、モジュールの読み込みが完了するまでブロックするため、慎重に使用しましょう。
-
Promise.race()を使ったタイムアウト処理:
- 長時間実行される可能性のある非同期処理には、タイムアウトを設定することを検討しましょう。
まとめ
TypeScriptにおけるasync/awaitは、非同期処理を同期的に書けるようにする強力な機能です。Promiseベースの非同期コードを読みやすく、理解しやすく、保守しやすくします。
この記事では、async/awaitの基本的な概念から実践的なパターンまでを初心者向けに解説しました。以下が主なポイントです:
- JavaScriptは基本的にシングルスレッドだが、非同期処理によって応答性の高いアプリケーションを作ることができる
-
Promiseは非同期処理の結果を表現するオブジェクトで、async/awaitはそれをさらに扱いやすくする構文 -
asyncキーワードは関数がPromiseを返すことを示し、awaitキーワードはPromiseが解決されるまで実行を一時停止する - エラーハンドリングには
try/catchブロックを使用する
非同期処理は最初は複雑に感じるかもしれませんが、async/awaitを使うことで、同期的なコードに近い感覚で書けるようになります。継続的な練習と実践を通じて、非同期プログラミングのスキルを向上させていきましょう。
何か不備などのご指摘があれば教えていただけると嬉しいです!
Discussion