Promise<T>
Promise
はES2015から追加された機能で、非同期処理を見通しよく書くことができます。ES2017で導入されたasync
/await
を使うことでPromise
で書いたコードをさらに見通しよく書くことができます。
Promise
がなかった時代のこと
次の3つのAPIがあるとしてこれらで得た結果を表示する処理を考えてみます。
- API1: リクエストを送り、結果を受け取る
- API2: API1の結果を使ってリクエストを送り、結果を受け取る
- API3: API2の結果を使ってリクエストを送り、結果を受け取る
API1, API2, API3の通信をする関数request1()
, request2()
, request3()
は次のようになります。各関数のsetTimeout()
はAPI通信をしている部分の遅延を意味している程度に考えてください。
js
// API1. 非同期でAPIにリクエストを送って値を取得する処理functionrequest1 (callback ) {setTimeout (() => {// 1 は適当な例、なんでもいいですcallback (1);}, 1000);}// API2. 受け取った値を別のAPIにリクエストを送って値を取得する処理functionrequest2 (result1 ,callback ) {setTimeout (() => {callback (result1 + 1);}, 1000);}// API3. 受け取った値を別のAPIにリクエストを送って値を取得する処理functionrequest3 (result2 ,callback ) {setTimeout (() => {callback (result2 + 2);}, 1000);}
js
// API1. 非同期でAPIにリクエストを送って値を取得する処理functionrequest1 (callback ) {setTimeout (() => {// 1 は適当な例、なんでもいいですcallback (1);}, 1000);}// API2. 受け取った値を別のAPIにリクエストを送って値を取得する処理functionrequest2 (result1 ,callback ) {setTimeout (() => {callback (result1 + 1);}, 1000);}// API3. 受け取った値を別のAPIにリクエストを送って値を取得する処理functionrequest3 (result2 ,callback ) {setTimeout (() => {callback (result2 + 2);}, 1000);}
これらの関数を組み合わせて3つのAPIリクエストを順次実装すると次のようになります。
js
request1 ((result1 ) => {request2 (result1 , (result2 ) => {request3 (result2 , (result3 ) => {console .log (result3 );// @log: 4});});});
js
request1 ((result1 ) => {request2 (result1 , (result2 ) => {request3 (result2 , (result3 ) => {console .log (result3 );// @log: 4});});});
次のAPIにリクエストを投げるためにひとつ前の非同期なAPIリクエストの結果を待つ必要があり、関数の呼び出しが入れ子になってしまいます。
これをコールバック地獄と呼び、ネストが深くコードの記述が非常に複雑になってしまう問題があります。ちなみにコールバック地獄は英語でもCallback hellと呼びます。どの世界でも地獄は地獄です。
Promise
が解決してくれること
先ほどの例をPromise
を使って書き直してみます。
js
// 非同期でAPIにリクエストを投げて値を取得する処理functionrequest1 () {return newPromise ((resolve ) => {setTimeout (() => {resolve (1);}, 1000);});}// 受け取った値を別のAPIにリクエストを投げて値を取得する処理functionrequest2 (result1 ) {return newPromise ((resolve ) => {setTimeout (() => {resolve (result1 + 1);}, 1000);});}// 受け取った値を別のAPIにリクエストを投げて値を取得する処理functionrequest3 (result2 ) {return newPromise ((resolve ) => {setTimeout (() => {resolve (result2 + 2);}, 1000);});}
js
// 非同期でAPIにリクエストを投げて値を取得する処理functionrequest1 () {return newPromise ((resolve ) => {setTimeout (() => {resolve (1);}, 1000);});}// 受け取った値を別のAPIにリクエストを投げて値を取得する処理functionrequest2 (result1 ) {return newPromise ((resolve ) => {setTimeout (() => {resolve (result1 + 1);}, 1000);});}// 受け取った値を別のAPIにリクエストを投げて値を取得する処理functionrequest3 (result2 ) {return newPromise ((resolve ) => {setTimeout (() => {resolve (result2 + 2);}, 1000);});}
戻り値がPromise
になり、コールバック関数を示す引数がなくなりました。Promise
を返す関数を使うと次のように3つのAPIリクエストを実装できます。
js
request1 ().then ((result1 ) => {returnrequest2 (result1 );}).then ((result2 ) => {returnrequest3 (result2 );}).then ((result3 ) => {console .log (result3 );// @log: 4});
js
request1 ().then ((result1 ) => {returnrequest2 (result1 );}).then ((result2 ) => {returnrequest3 (result2 );}).then ((result3 ) => {console .log (result3 );// @log: 4});
先ほどのコールバックの例と比べると非常にスッキリ書けるようになりました。
Promise
とジェネリクス
TypeScriptでPromise
の型を指定する場合はジェネリクスを伴いPromise<T>
と書きます。T
にはPromise
が履行された(fulfilled)ときに返す値の型を指定します。今回の例ではresolve(1)
と履行する値として数値を渡しているのでPromise<number>
を指定しています。
たとえば、独自で定義した型の値を履行する場合は次のように記述します。
ts
typeUser = {name : string;age : number;};functiongetUser ():Promise <User > {return newPromise ((resolve ) => {constuser :User = {name : "太郎",age : 10,};resolve (user );});}getUser ().then ((user :User ) => {console .log (user );// @log: { "name": "太郎", "age": 10 }});
ts
typeUser = {name : string;age : number;};functiongetUser ():Promise <User > {return newPromise ((resolve ) => {constuser :User = {name : "太郎",age : 10,};resolve (user );});}getUser ().then ((user :User ) => {console .log (user );// @log: { "name": "太郎", "age": 10 }});
Promise
のジェネリクスの型T
は必須なので、省略した場合はコンパイルエラーになります。
ts
functionGeneric type 'Promise<T>' requires 1 type argument(s).2314Generic type 'Promise<T>' requires 1 type argument(s).request ():{ Promise return newPromise ((resolve ) => {resolve (1);});}
ts
functionGeneric type 'Promise<T>' requires 1 type argument(s).2314Generic type 'Promise<T>' requires 1 type argument(s).request ():{ Promise return newPromise ((resolve ) => {resolve (1);});}
ジェネリクスの型T
と返す値の型が合わない場合もコンパイルエラーになります。
ts
functionrequest ():Promise <string> {return newPromise ((resolve ) => {// string型を期待しているが、number型を返しているのでコンパイルエラーArgument of type 'number' is not assignable to parameter of type 'string | PromiseLike<string>'.2345Argument of type 'number' is not assignable to parameter of type 'string | PromiseLike<string>'.resolve (1 );});}
ts
functionrequest ():Promise <string> {return newPromise ((resolve ) => {// string型を期待しているが、number型を返しているのでコンパイルエラーArgument of type 'number' is not assignable to parameter of type 'string | PromiseLike<string>'.2345Argument of type 'number' is not assignable to parameter of type 'string | PromiseLike<string>'.resolve (1 );});}
Promise
のメソッド
Promise<T>
には覚えておくべきメソッドが3つあります。
待ち受けた非同期処理の結果をコールバックで実行する - Promise.prototype.then()
Promise<T>
が履行された(fulfilled)ときに呼び出されます。引数に使われるコールバックの第1引数はT
型の値です。
コールバックの戻り値としてS
型またはPromise<S>
型の値を返すとPromise<S>
型を返します。
ts
constpromise1 :Promise <number> =Promise .resolve (1);constpromise2 :Promise <string> =promise1 .then ((value ) => `${value }`);
ts
constpromise1 :Promise <number> =Promise .resolve (1);constpromise2 :Promise <string> =promise1 .then ((value ) => `${value }`);
上記例はPromise.prototype.then()
のたびに新しく定数を定義していますが。上述のとおりPromise.prototype.then()
でメソッドチェーンできます。
ts
constpromise :Promise <boolean> =Promise .resolve ("1").then ((value ) =>Number (value )) // Promise<number>型になる.then ((value ) =>value > 0); // Promise<boolean>型になる
ts
constpromise :Promise <boolean> =Promise .resolve ("1").then ((value ) =>Number (value )) // Promise<number>型になる.then ((value ) =>value > 0); // Promise<boolean>型になる
コールバック内で例外を投げるとそのPromiseは拒否されます。
ts
Promise .resolve (1).then (() => {throw newError ();}).then (() => {console .log ("fulilled");}).catch (() => {console .log ("rejected");});
ts
Promise .resolve (1).then (() => {throw newError ();}).then (() => {console .log ("fulilled");}).catch (() => {console .log ("rejected");});
同様に、コールバック内で拒否されたPromise
を返すとそのPromiseは拒否されます。
ts
Promise .resolve (1).then (() => {returnPromise .reject (newError ());}).then (() => {console .log ("fulilled");}).catch (() => {console .log ("rejected");});
ts
Promise .resolve (1).then (() => {returnPromise .reject (newError ());}).then (() => {console .log ("fulilled");}).catch (() => {console .log ("rejected");});
待ち受けた非同期処理の拒否の結果をコールバックで実行する - Promise.prototype.catch()
Promise<T>
が拒否された(rejected)ときに呼び出されます。引数に使われるコールバックの第1引数はany
型の値です。
これもコールバックの戻り値としてS
型またはPromise<S>
型の値を返すとPromise<S>
型を返します。
ts
constpromise1 :Promise <number> =Promise .reject (newError ());constpromise2 :Promise <string> =promise1 .catch ((e ) =>e .message );
ts
constpromise1 :Promise <number> =Promise .reject (newError ());constpromise2 :Promise <string> =promise1 .catch ((e ) =>e .message );
Promise.prototype.catch()
はPromise
が履行されている状態だと実行されません。そのためPromise.prototype.catch()
のあとにPromise.prototype.then()
をつなげると実行されたときの型と実行されなかったときの型の両方を考える必要があります。
ts
Promise .resolve (1).catch (() => {return "1";})// string | number型になる.then ((value : string | number) => {console .log (value );});
ts
Promise .resolve (1).catch (() => {return "1";})// string | number型になる.then ((value : string | number) => {console .log (value );});
ただしPromise.prototype.catch()
のあとにPromise.prototype.then()
を書くというより、Promise.prototype.then()
のあとにPromise.prototype.catch()
を書くほうが多いでしょう。
ts
Promise .resolve (1).then ((num : number) => {return `${num }`;}).then ((str : string) => {returnstr .length > 1;}).catch ((e : any) => {console .log (e .message );});
ts
Promise .resolve (1).then ((num : number) => {return `${num }`;}).then ((str : string) => {returnstr .length > 1;}).catch ((e : any) => {console .log (e .message );});
待ち受けた非同期処理が終了次第コールバックを実行する - Promise.prototype.finally()
Promise<T>
が決定された(settled)ときに呼び出されます。コールバックに引数はありません。
このメソッドは戻り値を設定することはできません。
Promise.prototype.finally()
はES2018になって追加されました。
Promise
の静的メソッド
静的メソッドでも覚えておくべき大事なメソッドがあります。
すべての非同期処理の結果を待ち受ける - Promise.all()
第1引数に要素がPromise
の配列を取り、それらの実行結果を非同期で待ち受けます。戻り値はPromise
が解決される時間にかかわらず配列に与えられた順番どおりにPromiseの結果が返ります。
ts
functionrequest1 ():Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (1);}, 4000);});}functionrequest2 ():Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (2);}, 2000);});}functionrequest3 ():Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (3);}, 1000);});}Promise .all ([request1 (),request2 (),request3 ()]).then (([num1 ,num2 ,num3 ]) => {// request1が一番終了するまで遅いが結果の順番は保持され、num1がrequest1の結果になるconsole .log (num1 ,num2 ,num3 );// @log: 1, 2, 3});
ts
functionrequest1 ():Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (1);}, 4000);});}functionrequest2 ():Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (2);}, 2000);});}functionrequest3 ():Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (3);}, 1000);});}Promise .all ([request1 (),request2 (),request3 ()]).then (([num1 ,num2 ,num3 ]) => {// request1が一番終了するまで遅いが結果の順番は保持され、num1がrequest1の結果になるconsole .log (num1 ,num2 ,num3 );// @log: 1, 2, 3});
与えられたPromise
のうちひとつでも拒否された場合Promise.all()
は拒否されます。
ts
functionrequest1 ():Promise <number> {return newPromise ((resolve ,reject ) => {setTimeout (() => {reject (newError ("failed1"));}, 4000);});}functionrequest2 ():Promise <number> {return newPromise ((resolve ,reject ) => {setTimeout (() => {reject (newError ("failed2"));}, 2000);});}functionrequest3 ():Promise <number> {return newPromise ((resolve ,reject ) => {setTimeout (() => {reject (newError ("failed3"));}, 1000);});}Promise .all ([request1 (),request2 (),request3 ()]).then (([num1 ,num2 ,num3 ]) => {console .log (num1 ,num2 ,num3 );}).catch ((e ) => {// 最も早く終わった例外が返るconsole .log (e .message );// @log: 'failed3'});
ts
functionrequest1 ():Promise <number> {return newPromise ((resolve ,reject ) => {setTimeout (() => {reject (newError ("failed1"));}, 4000);});}functionrequest2 ():Promise <number> {return newPromise ((resolve ,reject ) => {setTimeout (() => {reject (newError ("failed2"));}, 2000);});}functionrequest3 ():Promise <number> {return newPromise ((resolve ,reject ) => {setTimeout (() => {reject (newError ("failed3"));}, 1000);});}Promise .all ([request1 (),request2 (),request3 ()]).then (([num1 ,num2 ,num3 ]) => {console .log (num1 ,num2 ,num3 );}).catch ((e ) => {// 最も早く終わった例外が返るconsole .log (e .message );// @log: 'failed3'});
履行されたPromise
を返す - Promise.resolve()
履行されたPromise
を返します。
ts
constpromise :Promise <number> =Promise .resolve (4);
ts
constpromise :Promise <number> =Promise .resolve (4);
拒否されたPromise
を返す - Promise.reject()
拒否されたPromise
を返します。
ts
constpromise :Promise <string> =Promise .reject (newError ("failed"));
ts
constpromise :Promise <string> =Promise .reject (newError ("failed"));
Promise
を履行、拒否にかかわらずすべて待ち受ける - Promise.allSettled()
第1引数に与えられたすべてのPromise
が決定される(settled)まで実行します。決定とは履行か拒否のことであり、ひとつでも拒否されると終了するPromise.all()
と異なり、すべてが履行されるか拒否されるまで処理を待ちます。
戻り値は判別可能なユニオン型として返ります。
📄️ 判別可能なユニオン型
TypeScriptの判別可能なユニオン型は、ユニオンに属する各オブジェクトの型を区別するための「しるし」がついた特別なユニオン型です。オブジェクトの型からなるユニオン型を絞り込む際に、分岐ロジックが複雑になる場合は、判別可能なユニオン型を使うとコードの可読性と保守性がよくなります。
Promise.allSettled()
はES2020になって追加されました。
ts
functionrequest1 ():Promise <number> {returnPromise .resolve (1);}functionrequest2 ():Promise <number> {returnPromise .reject (newError ("failed"));}Promise .allSettled ([request1 (),request2 ()]).then ((values ) => {console .log (values );// @log: { status: "fulfilled", value: 1}, { status: "rejected", reason: {}}// reason はエラーのオブジェクト});
ts
functionrequest1 ():Promise <number> {returnPromise .resolve (1);}functionrequest2 ():Promise <number> {returnPromise .reject (newError ("failed"));}Promise .allSettled ([request1 (),request2 ()]).then ((values ) => {console .log (values );// @log: { status: "fulfilled", value: 1}, { status: "rejected", reason: {}}// reason はエラーのオブジェクト});
いちばん初めに決定されたPromise
を返す - Promise.race()
Promise.all()
のように第1引数に要素がPromise
の配列を取り、それらをすべて非同期で実行しますが、その中のうちもっとも早く決定されたPromise
の結果を履行、拒否に関係なく返します。
ts
functionrequest1 ():Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (1);}, 4000);});}functionrequest2 ():Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (2);}, 2000);});}functionrequest3 ():Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (3);}, 1000);});}Promise .race ([request1 (),request2 (),request3 ()]).then ((num ) => {console .log (num );// @log: 3});
ts
functionrequest1 ():Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (1);}, 4000);});}functionrequest2 ():Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (2);}, 2000);});}functionrequest3 ():Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (3);}, 1000);});}Promise .race ([request1 (),request2 (),request3 ()]).then ((num ) => {console .log (num );// @log: 3});
次の例は一番初めに決定されるPromise
が拒否される場合の例です。
ts
functionrequest1 ():Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (1);}, 4000);});}functionrequest2 ():Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (2);}, 2000);});}functionrequest3 ():Promise <number> {return newPromise ((resolve ,reject ) => {setTimeout (() => {reject (newError ("failed"));}, 1000);});}Promise .race ([request1 (),request2 (),request3 ()]).then ((num ) => {console .log (num );}).catch ((e ) => {console .log (e .message );// @log: 'failed});
ts
functionrequest1 ():Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (1);}, 4000);});}functionrequest2 ():Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (2);}, 2000);});}functionrequest3 ():Promise <number> {return newPromise ((resolve ,reject ) => {setTimeout (() => {reject (newError ("failed"));}, 1000);});}Promise .race ([request1 (),request2 (),request3 ()]).then ((num ) => {console .log (num );}).catch ((e ) => {console .log (e .message );// @log: 'failed});
Promise
ふかぼり
Promise
の状態
文章中にも何度も出てきましたが、Promise
には3つの状態があります。
- pending
- fulfilled
- rejected
pendingは待機中という意味で、まだ待ち受けている非同期処理が完了していないときの状態を示します。fulfilledは履行という意味で、待ち受けている非同期処理が完了し、意図している状態(例外が発生しなかった)になったことを示します。rejectedは拒否という意味で、待ち受けている非同期処理が例外とともに完了したことを示します。
fulfilledとrejectedを合わせてsettledということがあります。このsettledは決定という意味です。