Menu

TypeScriptのPromise: Promise<T>、Promise.all、allSettled

TypeScriptでの Promise の型付けを解説します。Promise<T> 型、new Promise と resolve の型付け、then による型の変化、catch で any になる理由、タプルを返す Promise.all、Promise.allSettled の結果の型、コールバックのAPIを型付きの Promise でラップする方法です。

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

TypeScriptでは、Promise の型は Promise<T> で、T は解決される値の型です。Promise を返す関数は戻り値の型でそれを宣言し、then や await で得られる値は T 型になります。

Promise の仕組み(状態、マイクロタスクキュー、チェーン)は普通のJavaScriptで、JavaScriptのPromise で説明しています。このページは型についてです。

new Promise と resolve の型付け

new Promise が戻り値の型を宣言した関数の戻り値なら、T はそこから決まります。それ以外では、TypeScriptは resolve の呼び出しから T を推論せず、Promise<unknown> になります。型引数を明示的に渡しましょう:

細かい点が2つあります。resolve は T から型付けされるので、Promise<number> の中の resolve("42") はコンパイルエラーです。そして引数なしの resolve() が許可されるのは T が void を含むときだけで、Promise<number> ではエラー TS2794(Expected 1 arguments, but got 0. Did you forget to include 'void' in your type argument to 'Promise'?)になります。

reject の側は型付けされていません。Promise<T, E> というものはなく、Promise はどんな値でも reject できます。

then、catch、finally

then はそれぞれ、コールバックが返す値で型付けされた新しい Promise を返します。コールバックが Promise を返せばTypeScriptがそれを展開するので、Promise<Promise<T>> になることはありません。

標準ライブラリでは、catch のコールバックの引数は unknown ではなく any です。文字列かもしれない値に e.message と書いても止められません。上の例のように unknown と型注釈を付け、使う前に絞り込みましょう。await を try/catch で囲む方法のほうが優れています。strict のもとでは、その catch の変数は最初から unknown です(エラー処理 を参照)。

Promise.all はタプルを返す

型の違う Promise の配列リテラルを渡すと、Promise.all はタプルの Promise を返し、各位置がそれぞれの型を保ちます:

Promise ではない値を混ぜることもでき、それはそのまま通ります。Promise.all は入力が1つ reject するとすぐに reject し、ほかの結果は失われます。Promise.race は最初に確定したもので解決または reject し、型は入力のユニオン型です。Promise.any は最初に成功したもので解決し、すべての入力が失敗した場合にだけ AggregateError で reject します。

Promise.allSettled とその結果の型

Promise.allSettled はすべての入力を待ち、reject することはありません。各結果は PromiseSettledResult<T> で、status フィールドで絞り込むユニオン型です:

reject された側には value がないので、r.value は status のチェックのあとでしか存在しません。reason は catch と同じ理由で any です。filter では、絞り込んだ配列が fulfilled の結果として型付けされるように型述語を使っています。

コールバックのAPIを Promise でラップする

古いAPIは結果をコールバックで返し、多くは Node スタイルの (err, result) => void です。型付きの Promise を返す関数で一度ラップすれば、残りのコードで await が使えます:

Node では、(err, result) の規約に従う関数なら util.promisify がこれを行ってくれますし、多くの組み込みモジュールにはすでに Promise 版があります(node:fs/promises、node:timers/promises)。

よくある間違い

  • シグネチャが Promise<T> なのに T を返す、またはその逆。 Promise<User> と宣言した async ではない関数は Promise を返さなければなりません。async 関数は自動的に Promise を返します。
  • Promise の処理を忘れる。 await、then、catch のない save(user); のような呼び出しは問題なくコンパイルでき、reject は未処理の reject になります(Node ではデフォルトでプロセスが終了します)。typescript-eslint のルール no-floating-promises でこれを捕まえられます。
  • .catch((e) => ...) の型を信用する。 その e は any です。unknown と型注釈を付けましょう。
  • すでに Promise を返すものを new Promise で囲む。 既存の Promise をそのまま返すか await しましょう。

よくある質問

TypeScriptの Promise<T> とは何ですか?

Promise<T> は、T 型の値で解決される Promise の型です。Promise<string> を返す関数が返す Promise は、then のコールバックや await で string を渡します。値なしで解決される Promise は Promise<void> です。

TypeScriptで new Promise に型を付けるには?

型引数を渡します: new Promise<number>((resolve, reject) => ...)。型引数がないと、TypeScriptは resolve の呼び出しから値の型を推論できず、結果は Promise<unknown> になります。何も渡さずに解決する Promise には new Promise<void>(resolve => ...) を使うと、引数なしの resolve() が許可されます。

Promise の catch のエラーは何型ですか?

any です。何でも throw や reject できるので、標準ライブラリでは catch のコールバックの reason 引数は any になっています。自分で unknown と型注釈を付け(.catch((e: unknown) => ...))、使う前に instanceof Error で絞り込みましょう。

TypeScriptで Promise.all の型はどうなりますか?

配列リテラルに対する Promise.all は、入力ごとに1要素を順番に持つタプル型を返します。Promise.all([getUser(), getCount()]) は Promise<[User, number]> なので、分割代入すると各値がそれぞれの型になります。同じ型の配列 T[] に対しては Promise<T[]> を返します。

Promise.all と Promise.allSettled の違いは何ですか?

Promise.all は入力のどれかが reject するとすぐに reject します。Promise.allSettled は常に解決し、PromiseSettledResult<T> オブジェクトの配列を返します。各要素は { status: "fulfilled", value } か { status: "rejected", reason } です。status をチェックしてそれぞれを絞り込みます。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める