TypeScriptで非同期処理を扱う基本!Promise(プロミス)とasync/awaitを初心者向けに徹底解説
生徒
TypeScriptの勉強を始めたのですが、非同期処理とかPromiseという言葉が出てきて混乱しています。これって一体何なんですか?
先生
非同期処理は、重い作業をしている間に他の作業を並行して進めるための仕組みですよ。TypeScriptやJavaScriptでは、ネットワーク通信などの待ち時間を有効に使うために欠かせない技術です。
生徒
待ち時間を有効に使うんですね。難しそうですが、初心者でも使いこなせるようになりますか?
先生
大丈夫です!Promiseの仕組みを料理の注文に例えて、基本的な書き方から便利なasync/awaitの使い方まで、順番に見ていきましょう!
1. 非同期処理とは何か?
プログラミングの世界には、同期処理と非同期処理という二つの大きな考え方があります。普段、私たちが書くプログラムは、上から下へと順番に実行されます。これを同期処理と呼びます。例えば、料理のレシピを上から順番に進めていくようなイメージです。前の工程が終わるまで、次の工程には進みません。
しかし、インターネットを通じてデータを取得する場合などは、データの到着を待つ時間が数秒かかることがあります。その間、プログラムが完全に止まってしまうと、ユーザーは画面を操作できなくなり、不便を感じてしまいます。そこで登場するのが非同期処理です。非同期処理とは、時間のかかる処理をバックグラウンドで行い、その完了を待たずに別の作業を先に進める仕組みのことです。
パソコンを触ったことがない方でも、レンジでチンをしながら野菜を切る場面を想像してみてください。レンジが鳴るのをじっと立って待つのが同期処理、レンジに任せている間に包丁を使うのが非同期処理です。効率よく作業を進めるためには、この非同期の考え方がとても重要になります。
2. Promise(プロミス)の基本概念
非同期処理を扱う際に、TypeScriptで中心となるのがPromiseという仕組みです。Promiseを直訳すると「約束」という意味になります。これは「今はまだ結果が出ていないけれど、将来的に結果を返すことを約束する」というオブジェクトです。
例えば、レストランで料理を注文したときに渡される、呼び出しベルを想像してください。注文した瞬間には、手元に料理はありませんが、「料理ができたらベルを鳴らしてお知らせします」という約束が手渡されます。このベルこそがPromiseです。ベルを受け取った後、あなたは席に座ってスマホを見たり会話を楽しんだりできますよね。そして、料理が完成した(約束が果たされた)ときにベルが鳴り、料理を受け取ることができます。
このように、時間のかかる処理の結果を後で受け取るための予約票のような役割を果たすのが、TypeScriptのPromiseなのです。Promiseには、処理が成功した状態、失敗した状態、そしてまだ結果を待っている状態の三つがあります。
3. Promiseの状態変化について知ろう
Promiseには三つの状態があります。これを理解すると、非同期処理の挙動がぐっと分かりやすくなります。
- 待機(Pending):処理がまだ終わっておらず、結果を待っている状態です。
- 履行(Fulfilled):処理が無事に成功し、約束が果たされた状態です。
- 拒否(Rejected):何らかのエラーが発生し、約束が守れなかった状態です。
一度、成功(履行)または失敗(拒否)の状態になると、そこから状態が変わることはありません。プログラミングでは、この状態の変化に合わせて「成功したときはこの処理を、失敗したときはあの処理を」といった具合にコードを書いていきます。これにより、インターネットの通信が途切れてしまったときのような予期せぬ事態にも、丁寧に対応できるプログラムが作れるようになります。
4. 基本的なPromiseの書き方
それでは、実際にTypeScriptでPromiseをどのように書くのか見てみましょう。初心者の方向けに、最もシンプルな形でコードを書いてみます。まずは、Promiseを作成する側の書き方です。new Promiseという命令を使い、その中で成功時の関数と失敗時の関数を受け取ります。
const myPromise = new Promise((resolve, reject) => {
const isSuccess = true;
if (isSuccess) {
resolve("データの取得に成功しました!");
} else {
reject("エラーが発生しました。");
}
});
myPromise.then((message) => {
console.log(message);
}).catch((error) => {
console.log(error);
});
データの取得に成功しました!
このコードでは、resolveが成功を伝えるための関数、rejectが失敗を伝えるための関数です。thenというメソッドを使うことで、成功したときの結果を受け取ることができます。逆に、失敗したときはcatchを使ってエラー内容を受け取ります。これがPromiseの最も基礎的な形です。
5. asyncとawaitでコードをスッキリさせる
Promiseの書き方は非常に強力ですが、複数の処理を連続して行おうとすると、コードがどんどん深くなって読みづらくなることがあります。そこで導入されたのがasyncとawaitです。これを使うと、非同期処理をまるで普通の同期処理のように、上から順番に読みやすく書くことができます。
asyncは関数の前に付けて、「この関数の中では非同期処理を使いますよ」と宣言するキーワードです。そして、awaitはPromiseの結果が返ってくるまで処理を一時停止し、結果が出たら次に進むという魔法のような言葉です。これを使うことで、複雑なPromiseのチェーンを簡潔に表現できるようになります。
例えば、朝起きて、顔を洗って、朝食を食べるという一連の流れを、それぞれの準備に時間がかかる非同期処理として書く場合、async/awaitを使うと非常に見通しが良くなります。コードの見た目が整理されることで、開発ミスを減らす効果もあります。
6. async/awaitの実践的なコード例
では、具体的なコードを見ていきましょう。数秒待ってから挨拶を返す関数を作成し、それを呼び出す様子を再現します。setTimeoutという命令を使うことで、あえて時間をかける処理をシミュレーションしています。
function waitAndHello(name: string): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
resolve("こんにちは、" + name + "さん!");
}, 2000); // 2秒待つという意味です
});
}
async function sayHello() {
console.log("処理を開始します...");
const result = await waitAndHello("太郎");
console.log(result);
console.log("処理が完了しました。");
}
sayHello();
処理を開始します...
(2秒後)
こんにちは、太郎さん!
処理が完了しました。
このコードでは、awaitがあるおかげで、2秒待ってから次のconsole.logが実行されます。もしawaitを忘れてしまうと、まだ挨拶が準備できていない状態で次の処理が進んでしまい、期待通りの結果が得られません。非同期処理の結果をしっかり受け取ってから次へ進みたいときは、必ずセットで使うようにしましょう。
7. 複数の非同期処理を同時に扱う方法
非同期処理の真骨頂は、複数の処理を同時に並行して進めることができる点にあります。例えば、三つの異なるサーバーからデータを取得する場合、一つずつ順番に待っていると時間がかかりすぎてしまいます。そんな時に便利なのがPromise.allです。
これは、複数のPromiseをひとまとめにして、すべてが完了するのを一斉に待つための仕組みです。すべての処理が成功すれば、それぞれの結果を配列として受け取ることができます。料理でいえば、メインディッシュとスープとサラダを同時に作り始め、全部揃ったタイミングでテーブルに運ぶようなものです。効率を最大限に高めるために、非常に頻繁に使われるテクニックです。
function fetchTask(id: number): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
resolve("タスク" + id + "完了");
}, 1000);
});
}
async function runAllTasks() {
console.log("全てのタスクを開始します。");
const results = await Promise.all([
fetchTask(1),
fetchTask(2),
fetchTask(3)
]);
console.log(results);
console.log("全てのタスクが終わりました。");
}
runAllTasks();
全てのタスクを開始します。
(1秒後)
["タスク1完了", "タスク2完了", "タスク3完了"]
全てのタスクが終わりました。
このように、複数の処理を同時に進めることで、全体の待ち時間を大幅に短縮できます。個別に待つと3秒かかる処理も、同時に行えば約1秒で終わるというわけです。これが非同期処理をマスターする最大のメリットと言えるでしょう。
8. エラーハンドリングの重要性
非同期処理では、エラーへの備えが欠かせません。ネットワークの不具合や、指定したデータが見つからないといったトラブルは、プログラミングをしていると必ず直面します。Promiseではcatchを使いましたが、async/awaitを使う場合は、JavaScriptでおなじみのtry...catch構文を使用します。
tryブロックの中に実行したい処理を書き、もしその中でエラー(Promiseの拒否)が発生した場合は、即座にcatchブロックへと処理が移ります。これにより、エラーが起きてもプログラム全体がクラッシュして止まってしまうのを防ぎ、ユーザーに対して「通信に失敗しました」といった適切なメッセージを表示できるようになります。
async function errorHandlingExample() {
try {
console.log("データの取得を試みます...");
// 意図的にエラーを発生させるPromise
await new Promise((resolve, reject) => {
setTimeout(() => reject("サーバーが応答していません。"), 1500);
});
console.log("ここは実行されません。");
} catch (error) {
console.log("エラーをキャッチしました!内容:" + error);
} finally {
console.log("エラーの有無に関わらず、最後に必ず実行されます。");
}
}
errorHandlingExample();
データの取得を試みます...
(1.5秒後)
エラーをキャッチしました!内容:サーバーが応答していません。
エラーの有無に関わらず、最後に必ず実行されます。
このように、エラーが起きたときの逃げ道を用意しておくことは、信頼性の高いアプリケーションを作る上で非常に大切です。特に非同期処理は不確実な要素が多いため、常にエラーが起きる可能性を考えてコードを書く習慣をつけましょう。
9. 初心者がハマりやすいポイント
非同期処理を学び始めたばかりの方がよく陥る罠がいくつかあります。まず一つ目は、awaitを付け忘れることです。awaitを忘れると、関数はPromiseという予約票そのものを返してしまい、中身のデータを取り出すことができません。画面に「[object Promise]」と表示されてしまったら、まずawaitの有無を確認しましょう。
二つ目は、並行してできる処理を順番に待ってしまうことです。例えば、互いに関係のない二つのデータを取得する場合、一つ目の取得にawaitを使い、それが終わってから二つ目の取得にawaitを使うと、処理時間が倍かかってしまいます。先ほど紹介したPromise.allを活用して、効率化を図ることを意識してみてください。
最後に、型定義についてです。TypeScriptは型に厳しい言語ですので、Promiseがどのような種類のデータを返すのかをしっかりと指定する必要があります。例えば、文字列を返すならPromise<string>、数値を返すならPromise<number>といった具合です。これを正しく書くことで、エディタが強力にサポートしてくれるようになり、タイピングミスなどの初歩的なミスを防ぐことができます。