Promise / async / await
Promise
はES2015から追加された機能で、非同期処理を見通しよく書くことができます。ES2017で導入されたasync / await
を使うことでPromise
で書いたコードをさらに見通しよく書くことができます。
ここではPromise
の詳細な説明は割愛させて頂きます。
次のドキュメントが非常に分かりやすくまとめて頂いているので、最初にこちらを読み進めてPromise
について学ぶことをオススメします。
非同期処理:コールバック/Promise/Async Function · JavaScript Primer #jsprimer
ここでは、TypeScriptでPromise
を使う場合に注意する点を記載していきます。
コールバック地獄
次の3つのAPIがある時に、API3で得た結果を表示する処理を考えてみます。
- API1: 何かの値を返す
- API2: API1の結果をリクエストで受け取る
- API3: API2の結果をリクエストで受け取る
ts
typeCallback <T > = (result :T ) => void;// 非同期でAPIにリクエストを投げて値を取得する処理functionrequest1 (callback :Callback <number>) {setTimeout (() => {callback (1);}, 1000);}// 受け取った値を別のAPIにリクエストを投げて値を取得する処理functionrequest2 (result1 : number,callback :Callback <number>) {setTimeout (() => {callback (result1 + 1);}, 1000);}// 受け取った値を別のAPIにリクエストを投げて値を取得する処理functionrequest3 (result2 : number,callback :Callback <number>) {setTimeout (() => {callback (result2 + 2);}, 1000);}// コールバック地獄// 一つ前のAPIの結果を待って次のAPIをリクエストするために// コールバック関数が入れ子になってしまうrequest1 ((result1 ) => {request2 (result1 , (result2 ) => {request3 (result2 , (result3 ) => {console .log (result3 );// @log: 4});});});
ts
typeCallback <T > = (result :T ) => void;// 非同期でAPIにリクエストを投げて値を取得する処理functionrequest1 (callback :Callback <number>) {setTimeout (() => {callback (1);}, 1000);}// 受け取った値を別のAPIにリクエストを投げて値を取得する処理functionrequest2 (result1 : number,callback :Callback <number>) {setTimeout (() => {callback (result1 + 1);}, 1000);}// 受け取った値を別のAPIにリクエストを投げて値を取得する処理functionrequest3 (result2 : number,callback :Callback <number>) {setTimeout (() => {callback (result2 + 2);}, 1000);}// コールバック地獄// 一つ前のAPIの結果を待って次のAPIをリクエストするために// コールバック関数が入れ子になってしまうrequest1 ((result1 ) => {request2 (result1 , (result2 ) => {request3 (result2 , (result3 ) => {console .log (result3 );// @log: 4});});});
次のAPIにリクエストを投げるためにひとつ前の非同期なAPIリクエストの結果を待つ必要があり、関数の呼び出しが入れ子になってしまいます。
これをコールバック地獄と呼び、ネストが深くコードの記述が非常に複雑になってしまう問題があります。
Promise
とジェネリクス
先ほどの例をPromise
を使って書き直してみます。
ts
// 非同期でAPIにリクエストを投げて値を取得する処理functionrequest1 ():Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (1);}, 1000);});}// 受け取った値を別のAPIにリクエストを投げて値を取得する処理functionrequest2 (result1 : number):Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (result1 + 1);}, 1000);});}// 受け取った値を別のAPIにリクエストを投げて値を取得する処理functionrequest3 (result2 : number):Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (result2 + 2);}, 1000);});}request1 ().then ((result1 ) => {returnrequest2 (result1 );}).then ((result2 ) => {returnrequest3 (result2 );}).then ((result3 ) => {console .log (result3 );// @log: 4});
ts
// 非同期でAPIにリクエストを投げて値を取得する処理functionrequest1 ():Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (1);}, 1000);});}// 受け取った値を別のAPIにリクエストを投げて値を取得する処理functionrequest2 (result1 : number):Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (result1 + 1);}, 1000);});}// 受け取った値を別のAPIにリクエストを投げて値を取得する処理functionrequest3 (result2 : number):Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (result2 + 2);}, 1000);});}request1 ().then ((result1 ) => {returnrequest2 (result1 );}).then ((result2 ) => {returnrequest3 (result2 );}).then ((result3 ) => {console .log (result3 );// @log: 4});
先ほどのコールバックの例と比べると非常にスッキリ書けるようになりました。
ここで注目するべきはrequest1()
関数の戻り値をPromise<number>
と型指定をしている箇所です。
TypeScriptでPromise
の型を指定する場合はPromise<T>
と書きます。T
にはPromise
が解決(resolve)された時に渡す値の任意の型を指定します。
今回の例では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 );});}
async / await
Promise
を利用した非同期処理をより簡単に書ける構文としてasync /await
が存在します。
この構文を利用することで、非同期処理をより同期処理と同じような文脈で書くことができるようになります。
async
関数
関数の前にasync
キーワードをつけると、たとえその関数内でPromise
が返されていなくても、戻り値の型をPromise
で包んで返します。
ts
async functionrequestAsync ():Promise <number> {return 1;}
ts
async functionrequestAsync ():Promise <number> {return 1;}
この例では非Promise
である定数を返していますがasync
関数はその戻り値をPromise
で包んでいます。
ts
async functionrequestAsync ():Promise <number> {return 1;}// requestAsyncはこれと同じfunctionrequest ():Promise <number> {return newPromise ((resolve ) => {resolve (1);});}requestAsync ().then ((result ) => {console .log (result );// @log: 1});
ts
async functionrequestAsync ():Promise <number> {return 1;}// requestAsyncはこれと同じfunctionrequest ():Promise <number> {return newPromise ((resolve ) => {resolve (1);});}requestAsync ().then ((result ) => {console .log (result );// @log: 1});
Promise
をそのまま返すことも可能です。二重にPromise
がラップされることはありません。
ts
async functionrequestAsync ():Promise <number> {return newPromise ((resolve ) => {resolve (1);});}requestAsync ().then ((result ) => {console .log (result );// @log: 1});
ts
async functionrequestAsync ():Promise <number> {return newPromise ((resolve ) => {resolve (1);});}requestAsync ().then ((result ) => {console .log (result );// @log: 1});
await
await
はPromise
の値が解決されるまで実行を待機して、解決された値を返します。
await
の注意点として**await
はasync
関数の中でのみ使えます。**
ts
// 1秒後に値を返すfunctionrequest ():Promise <string> {return newPromise ((resolve ) => {setTimeout (() => {resolve ("hello");}, 1000);});}// この書き方はできない// const result = await request();// console.log(result);async functionmain () {constresult = awaitrequest ();console .log (result );// @log: "hello"}main ();
ts
// 1秒後に値を返すfunctionrequest ():Promise <string> {return newPromise ((resolve ) => {setTimeout (() => {resolve ("hello");}, 1000);});}// この書き方はできない// const result = await request();// console.log(result);async functionmain () {constresult = awaitrequest ();console .log (result );// @log: "hello"}main ();
この例ではawait request()
の行でrequest()
がPromise
を解決するまで1秒待機し、コンソールに"hello"
と表示します。
async / await
で書き直す
最後に3つのAPI呼び出しのコードをasync / await
を利用して書き直してみます。
このようにasync / await
を利用することで、非同期の処理を同期処理のようにスッキリ書くことができるようになります。
ts
// 非同期でAPIにリクエストを投げて値を取得する処理functionrequest1 ():Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (1);}, 1000);});}// 受け取った値を別のAPIにリクエストを投げて値を取得する処理functionrequest2 (result1 : number):Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (result1 + 1);}, 1000);});}// 受け取った値を別のAPIにリクエストを投げて値を取得する処理functionrequest3 (result2 : number):Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (result2 + 2);}, 1000);});}async functionmain () {constresult1 = awaitrequest1 ();constresult2 = awaitrequest2 (result1 );constresult3 = awaitrequest3 (result2 );console .log (result3 );// @log: 4}main ();
ts
// 非同期でAPIにリクエストを投げて値を取得する処理functionrequest1 ():Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (1);}, 1000);});}// 受け取った値を別のAPIにリクエストを投げて値を取得する処理functionrequest2 (result1 : number):Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (result1 + 1);}, 1000);});}// 受け取った値を別のAPIにリクエストを投げて値を取得する処理functionrequest3 (result2 : number):Promise <number> {return newPromise ((resolve ) => {setTimeout (() => {resolve (result2 + 2);}, 1000);});}async functionmain () {constresult1 = awaitrequest1 ();constresult2 = awaitrequest2 (result1 );constresult3 = awaitrequest3 (result2 );console .log (result3 );// @log: 4}main ();