Menu

TypeScriptでsleep: 処理を待つ・遅延させる方法

TypeScriptには組み込みの sleep はありませんが、1行で作れます。setTimeout で解決される Promise<void> を返す関数です。await での使い方、ループの中での待機、遅延付きのリトライ、sleep のキャンセル、そしてJavaScriptに処理をブロックする sleep がない理由を解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

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 やワーカーでスレッドをブロックできますが、これは低レベルなワーカー間の調整のためのもので、遅延のためのものではありません。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める