TypeScriptでsetTimeoutをPromise化!非同期処理とasync/awaitを徹底解説
生徒
「TypeScriptで、数秒待ってから処理を実行したいのですが、コードがぐちゃぐちゃになってしまいます。もっときれいに書く方法はありませんか?」
先生
「それは非同期処理の悩みですね。標準のsetTimeoutをPromiseという仕組みで包んであげると、asyncやawaitを使ってスッキリ書けるようになりますよ。」
生徒
「Promise化すると、具体的に何が便利になるのでしょうか?」
先生
「処理を順番に並べて書けるようになるので、読みやすさが劇的に向上します。さっそく、その魔法のような書き方を学んでいきましょう!」
1. 非同期処理とは何かを理解しよう
プログラミングの世界には、同期処理と非同期処理という二つの大きな考え方があります。パソコンに慣れていない方向けに、身近な例で解説します。
同期処理とは、一つの作業が終わるまで次の作業を絶対に始めないやり方です。例えば、レジに並んで自分の番が来るまでじっと待っている状態です。これに対し、非同期処理とは、時間がかかる作業を誰かに頼んでおいて、自分は別の作業を進めるやり方です。レストランで料理を注文した後、料理が届くまでの間にスマホを見たり会話を楽しんだりするイメージです。
TypeScriptやJavaScriptにおいて、この非同期処理は非常に重要です。なぜなら、データの読み込みやタイマーの待ち時間などで画面が止まってしまわないようにするために欠かせない技術だからです。特に、一定時間後に何かを動かすsetTimeoutは、非同期処理の代表格です。
2. 従来のsetTimeoutが抱える課題
TypeScriptで標準的に用意されているsetTimeoutは、指定したミリ秒後に処理を実行するための関数です。しかし、これには初心者泣かせの大きな問題があります。それは、処理が複雑になるとコールバック地獄と呼ばれる、階段のような読みにくいコードになってしまうことです。
例えば、3秒待ってから挨拶し、さらに2秒待ってから別のメッセージを出すといった処理を書こうとすると、関数の中に関数を書くという入れ子構造が発生します。これを繰り返すと、コードの右側がどんどん深くなり、どこで何をしているのか自分でも分からなくなってしまいます。この問題を解決するために登場したのが、今回学習するPromise化という技法です。
3. Promiseとは何かをやさしく解説
Promiseとは、直訳すると「約束」という意味です。プログラミングにおいては、今すぐには結果が出ないけれど、将来的に「成功したよ」あるいは「失敗したよ」という結果を返すことを約束するオブジェクトのことを指します。
Promiseには三つの状態があります。一つ目は待機中で、まだ結果が出ていない状態です。二つ目は成功で、無事に処理が終わった状態です。三つ目は失敗で、何らかの理由で処理がうまくいかなかった状態です。このPromiseという仕組みを使うことで、時間がかかる処理を扱いやすい部品として定義できるようになります。初心者のうちは、Promiseを「時間差で届く荷物を受け取るためのチケット」のように考えておくと分かりやすいでしょう。
4. setTimeoutをPromise化する具体的な方法
それでは、いよいよ本題のPromise化に挑戦しましょう。Promise化とは、古い形式の関数を新しく使いやすいPromise形式に変換することです。これを行うことで、後述するasync/awaitという非常に便利な書き方が使えるようになります。
基本となるのは、new Promiseという構文です。これを使って、指定したミリ秒だけ待機する自分専用のタイマー関数を作ってみましょう。以下のコードは、指定した時間だけプログラムの実行を止めるための雛形になります。
const sleep = (ms: number): Promise<void> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
};
このコードでは、msという名前で待ち時間を受け取ります。resolveは、処理が正常に終わったことを知らせるための合図です。setTimeoutが完了した瞬間に、このresolveを呼び出すことで、約束が果たされたことをプログラムに伝えています。
5. asyncとawaitの魔法でコードを劇的に綺麗にする
Promise化した関数を最大限に活かすのが、asyncとawaitというキーワードです。これらを使うと、非同期処理をまるで普通の同期処理のように上から下へと順番に書くことができます。
asyncは関数の前に付けて、「この関数の中では特別な待機処理を行いますよ」と宣言するために使います。一方、awaitはPromiseを返す関数の前に付けて、「この処理が終わるまでここで待ってください」という指示を出します。これらを組み合わせることで、従来の入れ子構造だったコードが驚くほどスッキリします。
const showMessages = async () => {
console.log("処理を開始します");
// 2秒待機する(Promise化した関数を呼び出し)
await sleep(2000);
console.log("2秒経過しました");
// さらに1秒待機する
await sleep(1000);
console.log("さらに1秒、合計3秒経過しました");
};
showMessages();
実行結果は以下のようになります。しっかりと指定した秒数待ってから文字が表示されます。
処理を開始します
(2秒待機)
2秒経過しました
(1秒待機)
さらに1秒、合計3秒経過しました
6. setIntervalをPromise化して特定の回数繰り返す
次に、一定の間隔で何度も処理を行うsetIntervalのPromise化について考えましょう。通常のsetIntervalは永遠に繰り返してしまいますが、Promise化することで「指定した回数だけ繰り返したら終了する」という使い勝手の良い機能を作ることができます。
これを実現するには、繰り返しの中で先ほど作成した待機関数を利用するのが最も効率的です。繰り返し処理を行うfor文と組み合わせることで、ループのたびに一定時間停止させることができます。これにより、カウントダウンタイマーのような機能を簡単に実装できます。
const startCountdown = async (count: number) => {
console.log("カウントダウン開始!");
for (let i = count; i > 0; i--) {
console.log("残り: " + i + "秒");
// 1秒待機する
await sleep(1000);
}
console.log("時間になりました!");
};
startCountdown(3);
実行結果は以下の通りです。一秒ごとに数字が減っていく様子が分かります。
カウントダウン開始!
残り: 3秒
残り: 2秒
残り: 1秒
時間になりました!
7. 型定義を意識した安全なプログラム作り
TypeScriptを使う最大のメリットは、データに型を付けてミスを未然に防ぐことです。今回作成した関数でも、しっかりと型を意識しましょう。例えば、待ち時間を指定する引数には、数字であることを示すnumber型を指定します。
また、Promiseが何を返すのかも定義できます。今回は単に待つだけなので、何も返さないことを意味するvoidを使い、Promise<void>と書きます。もし待機した後に計算結果などを返したい場合は、Promise<number>のように指定します。このように型を明示することで、他の人があなたのコードを見たときに、何を入力して何が返ってくるのかが一目で分かるようになります。これは、大規模な開発やチームでの作業において非常に重要な習慣です。
8. 実践的な活用例:データの擬似的な読み込み
実際のWeb開発では、サーバーからデータを取得する際に時間がかかることがあります。そんな時、本物のサーバーを用意しなくても、Promise化したタイマーを使って「データを読み込んでいるフリ」をさせることができます。これは開発中のテストにおいて非常に便利です。
以下の例では、ユーザー情報を取得するのに時間がかかる状況を再現しています。データを取得中というメッセージを出し、数秒後に実際のデータを返します。このように、非同期処理をマスターすると、ユーザーにストレスを与えないスムーズな動きをシミュレートできるようになります。
interface User {
id: number;
name: string;
}
const fetchUserData = async (id: number): Promise<User> => {
console.log("ユーザー情報を取得しています...");
// サーバー通信の代わりに1.5秒待機
await sleep(1500);
return {
id: id,
name: "テック太郎"
};
};
const displayApp = async () => {
const user = await fetchUserData(1);
console.log("取得完了!ユーザー名: " + user.name);
};
displayApp();
このプログラムを実行すると、以下の結果が得られます。
ユーザー情報を取得しています...
(1.5秒後)
取得完了!ユーザー名: テック太郎
9. エラーハンドリングでトラブルに備える
プログラムは常に成功するとは限りません。何らかのトラブルで処理が止まってしまうこともあります。非同期処理において、エラーを適切に扱うことは非常に重要です。async/awaitを使っている場合は、try...catchという構文を使ってエラーを捕まえることができます。
tryのブロックの中に実行したい処理を書き、もしそこでエラーが起きたらcatchのブロックへ移動して後始末をする、という流れです。これを忘れると、プログラムが予期せぬ場所で強制終了してしまう可能性があるため、必ずセットで覚えるようにしましょう。初心者の段階からこの「もしも」を考える癖をつけておくと、信頼性の高いエンジニアになれる第一歩となります。複雑なネットワーク通信を伴う処理では特に必須の知識です。
10. 非同期処理をマスターするためのコツ
最後に、非同期処理の上達方法をお伝えします。まずは今回紹介したsleep関数を自分で一行ずつ書いてみることです。目で見るだけでなく、実際にキーボードを叩くことで、コードの構造が体に染み込んできます。また、ブラウザのデベロッパーツールなどを使って、実際にコードが動く様子を観察するのも効果的です。
最初はPromiseやasync/awaitが魔法のように見えて難しく感じるかもしれませんが、使っているうちに「単に順番待ちを整理しているだけだ」と気づく時が来ます。その感覚を掴めれば、モダンなWeb開発の基礎はバッチリです。TypeScriptは強力なツールですので、ぜひ楽しみながら学習を続けてください。小さな積み重ねが、やがて大きなアプリケーションを作る力に変わります。