TypeScriptには組み込みの sleep はありません。遅延のあとに setTimeout が解決する Promise<void> を返す関数を書き、async 関数の中でそれを await します:
await sleep(300) が一時停止させるのは main だけです。プログラムは止まらず、待っている間もタイマー、ほかの async 関数、I/O は動き続けます。
sleep 関数の仕組み
setTimeout(resolve, ms) は ms ミリ秒後に resolve が呼ばれるよう予約し、resolve を呼ぶと Promise が確定します。その Promise を await すると、それまで async 関数が中断されます。
<void> という型引数には意味があります。単独の new Promise(...) は resolve の呼び出しから型を推論しないので、これがないと sleep は Promise<unknown> を返します。そして void だからこそ resolve を値なしで呼べます。Promise<number> では、引数なしの resolve() はコンパイルエラーです。関数に戻り値の型を宣言するなら、型引数の代わりにそこに書けます:
setTimeout が保証するのは最小の時間で、正確な時間ではありません。コールバックは、遅延が過ぎてイベントループが空いたときに実行されます。数ミリ秒のずれは想定しておきましょう。
ループの中で sleep する
for や for...of ループの中で await sleep() を使うと、反復の間で一時停止します。処理のペースを調整する方法です(レート制限、ポーリング、ターミナルでのアニメーション):
これは forEach の中では動きません。forEach は各 async コールバックが返す Promise を無視するので、すべての反復が一度に始まり、何も待ちません。async/await のページで説明しているように、for か for...of ループを使いましょう。
遅延付きのリトライ
よくある実際の使い方です。処理を試し、失敗したら待ってからやり直し、待ち時間を毎回2倍にします(指数バックオフ)。
try の中の return await fn() には意味があります。await がないと、reject された Promise は catch が見る前に返されてしまいます。
キャンセルできる sleep
普通の sleep は中断できません。途中で待つのをやめるには(ユーザーがキャンセルを押した、シャットダウンが始まった)、AbortSignal を渡し、それが発火したらタイマーをクリアします:
長いタイマーがクリアされたので、プログラムは中断のすぐあとに終了します。clearTimeout がなければ、Node は5秒が経過するまで動き続けます。
Node の組み込みの Promise 版タイマー
Node には node:timers/promises モジュールに Promise 版の setTimeout が含まれているので、Node のコードではヘルパーを書く代わりにこれをインポートできます。解決する値と AbortSignal も受け付けます:
import { setTimeout as sleep } from "node:timers/promises";
async function main() {
await sleep(1000); // wait one second
const result = await sleep(500, "done"); // resolves with "done"
const ac = new AbortController();
await sleep(10_000, undefined, { signal: ac.signal }); // cancellable
}
型は @types/node(npm i -D @types/node)から提供されます。このモジュールはブラウザーには存在しないので、どこでも動かしたいなら1行のヘルパーを使いましょう。
ブロックする sleep はない
スレッドを持つ言語には、現在のスレッドをブロックする sleep があります。JavaScriptはイベントループを持つ1つのスレッドでコードを実行するので、ブロックする sleep はほかのすべてを止めてしまいます。タイマーは発火せず、ネットワークのレスポンスは処理されず、ブラウザーではページが応答しなくなります。時計が特定の時刻を過ぎるまで回り続けるビジーループはまさにそれをするもので、正しい方法になることはありません。
JavaScriptとTypeScriptで待つ方法は非同期の sleep です。関数は中断され、スレッドは空き、タイマーが発火すると関数が再開します。Atomics.wait は本当にブロックできますが、SharedArrayBuffer に対してだけで、ブラウザーではメインスレッドでの使用が禁止されています。これはワーカースレッドの調整のためのもので、遅延のためのものではありません。
よくある質問
TypeScriptで sleep するには?
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)); を定義し、async 関数の中で await sleep(1000) を呼びます。プログラムのほかの部分を止めずに、その関数を約1秒間一時停止します。
TypeScriptに組み込みの sleep 関数はありますか?
ありません。TypeScriptはJavaScriptに型を加えるだけで独自の実行時ライブラリを持たず、JavaScriptにも sleep はありません。Node ではインポートできます: import { setTimeout as sleep } from "node:timers/promises"。ブラウザーでは1行のヘルパーを書きます。
sleep に await が必要なのはなぜですか?
sleep(1000) はすぐに Promise を返します。それが解決されるまで周りの async 関数を一時停止させるのは await だけです。await がなければ、次の行がすぐに実行されます。await は async 関数の中(または ES モジュールのトップレベル)でしか使えません。
TypeScriptで同期的な sleep でスレッドをブロックできますか?
普通のコードではできません。JavaScriptはイベントループを持つ1つのスレッドで動くので、ビジーウェイトのループはタイマー、ネットワークのコールバック、ページ全体など、すべてを止めてしまいます。代わりに非同期の sleep を使いましょう。Atomics.wait は Node やワーカーでスレッドをブロックできますが、これは低レベルなワーカー間の調整のためのもので、遅延のためのものではありません。