TypeScriptの async 関数は常に Promise を返します。本体が string を返すなら、型は Promise<string> です。その中では、await が Promise<T> を T に展開します。
実行時の動き(await が何を一時停止するか、イベントループ)はJavaScriptのもので、JavaScriptのasync/await で説明しています。TypeScriptが担当するのは、入る型と出る型です。
async 関数の戻り値の型
本体は普通の値を返しますが、async 関数の宣言された戻り値の型は Promise<...> でなければなりません。省略すればTypeScriptが推論します。
async function count(): number と書くとエラー TS1064(The return type of an async function or method must be the global Promise<T> type. Did you mean to write 'Promise<number>'?)です。ユーティリティ型 Awaited<T> は展開した型を返します。Awaited<ReturnType<typeof count>> は number です。
await を忘れる
await を忘れると、T ではなく Promise<T> を持つことになります。型が合わなくなるので、TypeScriptはたいていこれを捕まえます:
if (ok) のバグは現実のものです。Promise オブジェクトは常に truthy なので、アクセスを許可してしまいました。TypeScriptはこれを TS2801 として報告します。Promise を boolean 型の変数に代入したり、Promise にないプロパティを読んだりしても、コンパイルエラーになります。結果を無視する呼び出し(save(user);)は捕まえられません。それには typescript-eslint のルール no-floating-promises を使います。
try/catch によるエラー処理
reject された Promise を await すると例外が投げられるので、普通の try/catch が使えます。strict のもとでは catch の変数は unknown なので、.message を読む前に絞り込みます:
async 関数の中の throw は、呼び出し箇所で例外を投げるのではなく、その関数の Promise を reject します。独自のエラークラスや、例外を投げずに結果を返す方法など、ほかのパターンは エラー処理 のページにあります。
トップレベルの await
関数の外の await が使えるのは ES モジュールだけです。CommonJS としてコンパイルされるファイル(このページの例や、"type": "module" のない Node のプロジェクト)では拒否されます:
index.ts(2,14): error TS1309: The current file is a CommonJS module and cannot use 'await' at the top level.
このページのすべての例と同じように、コードを async な main 関数で囲んで呼び出しましょう。ES モジュールのプロジェクト(package.json に "type": "module" があり、module が node16 か nodenext、またはバンドラー向けの module: "esnext")では、トップレベルの await が使えます。
逐次実行と並列実行
await はそれぞれ、Promise を待ってから次の行を始めます。互いに独立した呼び出しなら、先にすべてを開始し、Promise.all でまとめて await しましょう:
逐次実行の前半では、b はまだ始まっていないので a より先に終わることはありません。並列実行の後半では d が先に終わり、全体の時間は合計ではなく、いちばん長い呼び出しとほぼ同じになります。それでも Promise.all は結果を入力の順序で、タプルとして型付けして返します。
forEach の落とし穴
forEach は async のコールバックが返す Promise を無視するので、処理を待つものは何もありません:
await を使った for...of は要素を1つずつ処理し、map と組み合わせた Promise.all は並列に実行してすべてを待ちます。forEach はそのどちらでもなく、TypeScriptも警告しません。void を返すと型付けされたコールバックは、Promise を返すコールバックも受け付けるからです。
for await による非同期の反復
for await...of は、非同期ジェネレーターのような非同期イテラブルをループし、各値を await します:
async function* pages(total: number): AsyncGenerator<string[]> {
for (let page = 1; page <= total; page++) {
await new Promise((r) => setTimeout(r, 10));
yield [`item ${page}a`, `item ${page}b`];
}
}
async function main() {
for await (const batch of pages(3)) {
console.log(batch.join(", ")); // batch: string[]
}
}
main();
要素の型は、ジェネレーターの AsyncGenerator<T> の型注釈から、または省略した場合は推論から決まります。
よくある質問
TypeScriptの async 関数の戻り値の型は何ですか?
常に Promise です。number を返す async 関数の戻り値の型は Promise<number> で、何も返さない関数は Promise<void> です。async function f(): number と書くとエラー TS1064 になり、Promise<number> を使うよう提案されます。
TypeScriptでトップレベルの await を使うには?
トップレベルの await が使えるのは ES モジュールだけです。module を es2022、esnext、system、preserve、または Node が ESM として扱うファイルでの node16/node18/node20/nodenext にし、target を es2017 以上にする必要があります。CommonJS のファイルではエラー TS1309 です。どこでも使える解決策は async な main 関数です: async function main() { ... } main();。
TypeScriptで async/await のエラーを処理するには?
await を try/catch で囲みます。strict のもとでは捕捉した値は unknown 型なので、先に絞り込みます: if (e instanceof Error) console.log(e.message)。await も catch もされない reject は、未処理の reject になります。
TypeScriptで非同期の呼び出しを並列に実行するには?
先にすべての Promise を開始し、まとめて await します: const [a, b] = await Promise.all([loadA(), loadB()])。await loadA(); await loadB(); と書くと1つずつ順に実行されます。Promise.all は結果のタプルで各結果の型を保ちます。
forEach の中で async が動かないのはなぜですか?
forEach はコールバックを呼んで戻り値を無視するので、async のコールバックが返す Promise は await されません。ループはすぐに終わり、処理が終わる前にループのあとのコードが実行されます。1つずつ処理するなら await を使った for...of を、並列に処理するなら await Promise.all(items.map(async (x) => ...)) を使いましょう。