メインコンテンツまでスキップ

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
type Callback<T> = (result: T) => void;
 
// 非同期でAPIにリクエストを投げて値を取得する処理
function request1(callback: Callback<number>) {
setTimeout(() => {
callback(1);
}, 1000);
}
 
// 受け取った値を別のAPIにリクエストを投げて値を取得する処理
function request2(result1: number, callback: Callback<number>) {
setTimeout(() => {
callback(result1 + 1);
}, 1000);
}
 
// 受け取った値を別のAPIにリクエストを投げて値を取得する処理
function request3(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
type Callback<T> = (result: T) => void;
 
// 非同期でAPIにリクエストを投げて値を取得する処理
function request1(callback: Callback<number>) {
setTimeout(() => {
callback(1);
}, 1000);
}
 
// 受け取った値を別のAPIにリクエストを投げて値を取得する処理
function request2(result1: number, callback: Callback<number>) {
setTimeout(() => {
callback(result1 + 1);
}, 1000);
}
 
// 受け取った値を別のAPIにリクエストを投げて値を取得する処理
function request3(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にリクエストを投げて値を取得する処理
function request1(): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
}
 
// 受け取った値を別のAPIにリクエストを投げて値を取得する処理
function request2(result1: number): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(result1 + 1);
}, 1000);
});
}
 
// 受け取った値を別のAPIにリクエストを投げて値を取得する処理
function request3(result2: number): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(result2 + 2);
}, 1000);
});
}
 
request1()
.then((result1) => {
return request2(result1);
})
.then((result2) => {
return request3(result2);
})
.then((result3) => {
console.log(result3);
// @log: 4
});
ts
// 非同期でAPIにリクエストを投げて値を取得する処理
function request1(): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
}
 
// 受け取った値を別のAPIにリクエストを投げて値を取得する処理
function request2(result1: number): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(result1 + 1);
}, 1000);
});
}
 
// 受け取った値を別のAPIにリクエストを投げて値を取得する処理
function request3(result2: number): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(result2 + 2);
}, 1000);
});
}
 
request1()
.then((result1) => {
return request2(result1);
})
.then((result2) => {
return request3(result2);
})
.then((result3) => {
console.log(result3);
// @log: 4
});

先ほどのコールバックの例と比べると非常にスッキリ書けるようになりました。

ここで注目するべきはrequest1()関数の戻り値をPromise<number>と型指定をしている箇所です。

TypeScriptでPromiseの型を指定する場合はPromise<T>と書きます。TにはPromiseが解決(resolve)された時に渡す値の任意の型を指定します。

今回の例ではresolve(1);と解決する値として数値を渡しているのでPromise<number>を指定しています。

たとえば、独自で定義した型の値を解決する場合は次のように記述します。

ts
type User = {
name: string;
age: number;
};
 
function getUser(): Promise<User> {
return new Promise((resolve) => {
const user: User = {
name: "太郎",
age: 10,
};
resolve(user);
});
}
 
getUser().then((user: User) => {
console.log(user);
// @log: { "name": "太郎", "age": 10 }
});
ts
type User = {
name: string;
age: number;
};
 
function getUser(): Promise<User> {
return new Promise((resolve) => {
const user: User = {
name: "太郎",
age: 10,
};
resolve(user);
});
}
 
getUser().then((user: User) => {
console.log(user);
// @log: { "name": "太郎", "age": 10 }
});

Promiseのジェネリクスの型Tは必須なので、省略した場合はコンパイルエラーになります。

ts
function request(): Promise {
Generic type 'Promise<T>' requires 1 type argument(s).2314Generic type 'Promise<T>' requires 1 type argument(s).
return new Promise((resolve) => {
resolve(1);
});
}
ts
function request(): Promise {
Generic type 'Promise<T>' requires 1 type argument(s).2314Generic type 'Promise<T>' requires 1 type argument(s).
return new Promise((resolve) => {
resolve(1);
});
}

ジェネリクスの型Tと返す値の型が合わない場合もコンパイルエラーになります。

ts
function request(): Promise<string> {
return new Promise((resolve) => {
// string型を期待しているが、number型を返しているのでコンパイルエラー
resolve(1);
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>'.
});
}
ts
function request(): Promise<string> {
return new Promise((resolve) => {
// string型を期待しているが、number型を返しているのでコンパイルエラー
resolve(1);
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>'.
});
}

async / await

Promiseを利用した非同期処理をより簡単に書ける構文としてasync /awaitが存在します。
この構文を利用することで、非同期処理をより同期処理と同じような文脈で書くことができるようになります。

async関数

関数の前にasyncキーワードをつけると、たとえその関数内でPromiseが返されていなくても、戻り値の型をPromiseで包んで返します。

ts
async function requestAsync(): Promise<number> {
return 1;
}
ts
async function requestAsync(): Promise<number> {
return 1;
}

この例では非Promiseである定数を返していますがasync関数はその戻り値をPromiseで包んでいます。

ts
// これと同じ
function request(): Promise<number> {
return new Promise((resolve) => {
resolve(1);
});
}
 
requestAsync().then((result) => {
console.log(result);
// @log: 1
});
ts
// これと同じ
function request(): Promise<number> {
return new Promise((resolve) => {
resolve(1);
});
}
 
requestAsync().then((result) => {
console.log(result);
// @log: 1
});

Promiseをそのまま返すことも可能です。二重にPromiseがラップされることはありません。

ts
async function requestAsync(): Promise<number> {
return new Promise((resolve) => {
resolve(1);
});
}
 
requestAsync().then((result) => {
console.log(result);
// @log: 1
});
ts
async function requestAsync(): Promise<number> {
return new Promise((resolve) => {
resolve(1);
});
}
 
requestAsync().then((result) => {
console.log(result);
// @log: 1
});

await

awaitPromiseの値が解決されるまで実行を待機して、解決された値を返します。

awaitの注意点として**awaitasync関数の中でのみ使えます。**

ts
// 1秒後に値を返す
function request(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
resolve("hello");
}, 1000);
});
}
 
// この書き方はできない
// const result = await request();
// console.log(result);
 
async function main() {
const result = await request();
console.log(result);
// @log: "hello"
}
 
main();
ts
// 1秒後に値を返す
function request(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
resolve("hello");
}, 1000);
});
}
 
// この書き方はできない
// const result = await request();
// console.log(result);
 
async function main() {
const result = await request();
console.log(result);
// @log: "hello"
}
 
main();

この例ではawait request()の行でrequest()Promiseを解決するまで1秒待機し、コンソールに"hello"と表示します。

async / awaitで書き直す

最後に3つのAPI呼び出しのコードをasync / awaitを利用して書き直してみます。

このようにasync / awaitを利用することで、非同期の処理を同期処理のようにスッキリ書くことができるようになります。

ts
// 非同期でAPIにリクエストを投げて値を取得する処理
function request1(): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
}
 
// 受け取った値を別のAPIにリクエストを投げて値を取得する処理
function request2(result1: number): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(result1 + 1);
}, 1000);
});
}
 
// 受け取った値を別のAPIにリクエストを投げて値を取得する処理
function request3(result2: number): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(result2 + 2);
}, 1000);
});
}
 
async function main() {
const result1 = await request1();
const result2 = await request2(result1);
const result3 = await request3(result2);
console.log(result3);
// @log: 4
}
 
main();
ts
// 非同期でAPIにリクエストを投げて値を取得する処理
function request1(): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
}
 
// 受け取った値を別のAPIにリクエストを投げて値を取得する処理
function request2(result1: number): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(result1 + 1);
}, 1000);
});
}
 
// 受け取った値を別のAPIにリクエストを投げて値を取得する処理
function request3(result2: number): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(result2 + 2);
}, 1000);
});
}
 
async function main() {
const result1 = await request1();
const result2 = await request2(result1);
const result3 = await request3(result2);
console.log(result3);
// @log: 4
}
 
main();
  • 質問する ─ 読んでも分からなかったこと、TypeScriptで分からないこと、お気軽にGitHubまで🙂
  • 問題を報告する ─ 文章やサンプルコードなどの誤植はお知らせください。