TypeScriptでPromiseチェーンを安全に書く方法!非同期処理とasync/awaitを徹底解説
生徒
「TypeScriptで通信が終わるのを待ってから次の処理をしたいのですが、コードが複雑になってしまいます。安全に書くコツはありますか?」
先生
「それは非同期処理という技術ですね。TypeScriptではPromiseやasync、awaitを使うことで、まるで普通の計算のように順番通りに処理を書くことができますよ。」
生徒
「Promiseチェーンをきれいに保つ方法や、型安全にエラーを防ぐ方法について詳しく知りたいです!」
先生
「初心者の方でも安心して使えるように、基本から一歩ずつ安全な書き方をマスターしていきましょう!」
1. 非同期処理とPromiseの基本概念
プログラミングの世界には非同期処理という言葉があります。これは、ある作業が終わるのを待たずに、別の作業を先に進める仕組みのことです。例えば、料理をするときに、お湯が沸騰するのをじっと見守るのではなく、その間に野菜を切るのと似ています。パソコンの操作で言えば、インターネットから大きな画像をダウンロードしている間に、他の文字を入力できるような状態を指します。
TypeScriptやJavaScriptでこの非同期処理を扱うための道具がPromiseです。Promiseとは、直訳すると約束という意味になります。今はまだ結果が出ていないけれど、将来的に成功してデータが返ってくるか、あるいは失敗してエラーが出るか、どちらかの結果を届けることを約束するオブジェクトです。この仕組みを理解することが、安全なプログラムを書く第一歩となります。
2. Promiseチェーンが複雑になる理由
以前のプログラムでは、非同期処理が終わった後に実行したい内容を、関数の引数として渡していました。これをコールバック関数と呼びますが、処理が連続すると、関数の中にさらに関数が入るという入れ子構造が深くなってしまい、コードが非常に読みづらくなります。これを、専門用語でコールバック地獄と呼びます。
Promiseを使うと、この入れ子を解消して、上から下へ流れるように処理を繋げることができます。これをPromiseチェーンと呼びます。しかし、このチェーンも長く繋ぎすぎたり、途中でエラー処理を忘れたりすると、どこで問題が起きたのか分からなくなる危険があります。初心者が陥りやすい罠は、値を正しく次の処理に渡せなかったり、型をあいまいにしたまま進めてしまったりすることです。
3. 基本的なPromiseの書き方と実行順序
まずは、一番シンプルなPromiseの使い方を見てみましょう。Promiseには、成功を知らせるための関数と、失敗を知らせるための関数の二つが用意されています。これらを使って、数秒後にメッセージを表示するプログラムを書いてみます。TypeScriptを使う利点は、返ってくるデータの種類をあらかじめ決めておけることです。
// 1秒後に挨拶を返す約束(Promise)を作る
const greetingPromise = new Promise<string>((resolve) => {
setTimeout(() => {
resolve("こんにちは!TypeScriptの世界へようこそ!");
}, 1000);
});
// 約束が果たされたら中身を表示する
greetingPromise.then((message) => {
console.log(message);
});
こんにちは!TypeScriptの世界へようこそ!
上記のコードでは、thenというメソッドを使って、約束が果たされた後の処理を書いています。型としてstringを指定しているので、受け取るメッセージが文字列であることが保証されます。これにより、間違えて数字として扱ってしまうようなミスを未然に防ぐことができます。
4. Promiseチェーンを安全に繋ぐコツ
複数の非同期処理を順番に行いたい場合、thenを繋げていくことができます。ここで大切なのは、各thenの中で必ず次の値をreturnすることです。戻り値を忘れてしまうと、次の処理にデータが引き継がれず、意図しないエラーの原因となります。また、TypeScriptの型推論を活用することで、データの流れを常に監視することが可能です。
// ユーザーIDを取得する処理(擬似的な非同期処理)
function fetchUserId(): Promise<number> {
return Promise.resolve(101);
}
// ユーザー名を取得する処理
function fetchUserName(id: number): Promise<string> {
return Promise.resolve(`ユーザー名: 山田太郎 (ID: ${id})`);
}
// チェーンを繋いで実行
fetchUserId()
.then((id) => {
console.log("ID取得完了");
return fetchUserName(id); // 次のPromiseを返す
})
.then((name) => {
console.log(name);
})
.catch((error) => {
console.error("エラーが発生しました:", error);
});
ID取得完了
ユーザー名: 山田太郎 (ID: 101)
このように、一つの処理が終わったらその結果を使って次の関数を呼び出す、という流れがスムーズに記述できます。最後のcatchは、途中で何かトラブルが起きた場合に、そのエラーを一括で受け止めるためのセーフティネットの役割を果たします。
5. asyncとawaitで読みやすさを追求する
Promiseチェーンは便利ですが、さらに直感的に書く方法としてasync/awaitがあります。これは、非同期処理をまるで普通の同期的なコードと同じように書ける魔法のような構文です。関数の前にasyncを付け、待ちたい処理の前にawaitを書くだけで、プログラムはその行が終わるまで一時停止し、結果が出てから次の行へ進みます。これにより、見た目が劇的にスッキリします。
// 非同期で動くメインの処理
async function displayUserInfo() {
try {
console.log("データ取得を開始します...");
const id = await fetchUserId(); // ここで終わるまで待機
const name = await fetchUserName(id); // 次の処理も待機
console.log("すべてのデータを取得しました。");
console.log(name);
} catch (error) {
console.error("データの読み込みに失敗しました。", error);
}
}
displayUserInfo();
データ取得を開始します...
すべてのデータを取得しました。
ユーザー名: 山田太郎 (ID: 101)
この書き方の最大の特徴は、例外処理に馴染みのあるtry...catch構文が使えることです。これにより、成功したときの処理と、失敗したときの処理を明確に分けることができ、予期せぬプログラムの停止を防ぐことができます。パソコンに詳しくない方でも、手順書を一行ずつ読むような感覚でコードを追えるのがメリットです。
6. 型安全なエラーハンドリングの実践
TypeScriptを使う上で最も強力な武器は、エラーの可能性を型で表現できることです。例えば、通信は常に成功するとは限りません。サーバーが落ちていたり、インターネットが繋がっていなかったりすることもあります。そうした例外的な状況をあらかじめ想定して、どのようなエラーが返ってくるかを定義しておくことで、安全性が飛躍的に向上します。
初心者のうちは、エラーを握りつぶしてしまう(何もせずに放置する)ことがありますが、これは非常に危険です。必ずcatchブロックを用意し、ユーザーに状況を知らせるか、ログに記録するようにしましょう。TypeScriptではエラーの型をErrorオブジェクトとして扱うことが一般的です。これにより、メッセージの内容を確認して適切な対処ができるようになります。
7. 複数の処理を同時にこなすPromise.all
これまでは順番に一つずつ処理を待ってきましたが、時には複数の作業を同時に並行して行いたい場面もあります。例えば、複数のユーザー情報を一気に取得する場合です。一つずつ待っていると時間がかかりますが、まとめて依頼を出せば効率が良くなります。そのための機能がPromise.allです。
async function fetchMultipleData() {
const promise1 = Promise.resolve("データの断片A");
const promise2 = Promise.resolve("データの断片B");
const promise3 = Promise.resolve("データの断片C");
// 全ての約束が果たされるまで同時に待つ
const results = await Promise.all([promise1, promise2, promise3]);
console.log("全てのデータを取得しました!");
console.log(results); // 配列として結果が返ってくる
}
fetchMultipleData();
全てのデータを取得しました!
[ 'データの断片A', 'データの断片B', 'データの断片C' ]
Promise.allを使うと、渡したすべての処理が成功したときに、その結果を配列として受け取ることができます。もし、一つでも失敗すれば全体がエラー扱いになるため、中途半端にデータが欠けた状態で処理が進む心配がありません。このように、データの整合性を保つのも安全なプログラミングには欠かせない視点です。
8. タイムアウトを設けてフリーズを防ぐ
インターネットの通信では、返事がいつまでも返ってこないことがあります。そのまま待ち続けてしまうと、アプリが固まったように見えてしまいます。そこで、一定時間が経過したら強制的に失敗させるタイムアウトの仕組みを作ることが重要です。これもPromiseを応用することで実現可能です。
具体的には、本来の処理と「一定時間後にエラーを出す処理」の二つを用意し、どちらか早い方の結果を採用するようにします。これをPromise.raceと呼びます。こうした工夫を一つ加えるだけで、初心者向けの簡単なツールであっても、非常に信頼性の高い、プロのような仕上がりに近づけることができます。ユーザーを不安にさせないための配慮も、安全なコードの一部と言えるでしょう。
9. TypeScriptにおける非同期処理の注意点
最後に、よくあるミスを確認しておきましょう。最も多いのが、非同期関数を呼び出しているのにawaitを付け忘れることです。これを忘れると、関数は中身を実行する前にPromiseという入れ物だけを返してしまい、後の処理がめちゃくちゃになってしまいます。幸い、TypeScriptのエディタ(VSCodeなど)を使っていれば、このようなミスを警告してくれることがあります。
また、大きなループの中でawaitを使いすぎると、一つ一つの処理を順番に待つため、全体の動作が非常に遅くなることがあります。たくさんのデータを扱うときは、先ほど紹介したPromise.allを組み合わせて、並行して処理を行うように設計しましょう。基本的な文法だけでなく、このようなパフォーマンスへの意識を持つことが、脱初心者の鍵となります。