TL;DR
- 非同期処理は、組み合わせやすさと型の流れを考えて、コールバックではなく Promise で書く。
- できれば
Promiseを直接使わずに async/await を使う。可読性が上がり、戻り値がPromiseになることも強制できる。
内容
JavaScript の非同期処理の歴史
ES5 以前の JavaScript では、コールバックで非同期処理を表していた。
fetchURL(url1, function (response1) {
fetchURL(url2, function (response2) {
fetchURL(url3, function (response3) {
// ...
})
// ...
})
// ...
})ネストが深くなり(コールバック地獄)、実行順序がコードの順序と逆になるなど、可読性が低かった。
ES2015 で Promise が導入され、簡潔に書けるようになった。ネストが浅くなり、実行順序とコードの順序も一致した。Promise.all のような便利な関数も登場した。
const page1Promise = fetch(url1)
page1Promise
.then((response1) => {
return fetch(url2)
})
.then((response2) => {
return fetch(url3)
})
.then((response3) => {
// ...
})
.catch((error) => {
// ...
})ES2017 で async と await が導入され、さらに簡潔に書けるようになった。await は、各 Promise が完了するまで fetchPages の実行を一時停止する。await した Promise が reject されると、例外が投げられる。
async function fetchPages() {
try {
const response1 = await fetch(url1)
const response2 = await fetch(url2)
const response3 = await fetch(url3)
// ...
} catch (e) {
// ...
}
}TypeScript における async/await
reject された Promise が投げる例外は、通常の例外と同じく型を持たない。TypeScript では、ES5 以前をターゲットにしても async と await を使える。また、async 関数から Promise を返しても、Promise<Promise<T>> とネストせず Promise<T> になる。
async function getJSON(url: string) {
const response = await fetch(url)
const jsonPromise = response.json()
return jsonPromise // Promise<any>
} // 戻り値の型も Promise<any>コールバックより Promise が好ましい理由
Promise はコールバックより組み合わせやすく、型の流れもよくなる。たとえば Promise.all を使うと、各レスポンスの型が Response と推論される。
async function fetchPages() {
const [response1, response2, response3] = await Promise.all([
fetch(url1),
fetch(url2),
fetch(url3),
]) // response はすべて Response と推論される
// ...
}コールバックで同じことをすると、より多くのコードと型アノテーションが必要になる。エラー処理や、Promise.all のような汎用の関数を自前で用意するのも難しい。
Promise.race は、複数の Promise のうち最初に完了したものの結果を返す。次の関数の戻り値は Promise<Response> と推論される。never とのユニオンを取っても集合は変わらないので、timeout が返す Promise<never> との合併は Promise<Response | never> ではなく Promise<Response> に単純化される。
async function fetchWithTimeout(url: string, timeoutMs: number) {
return Promise.race([fetch(url), timeout(timeoutMs)])
}Promise より async/await が好ましい理由
1 つ目は、コードの可読性が上がることである。2 つ目は、async 関数は常に Promise を返すよう強制されることである。これにより、関数が同期的に値を返すのか非同期に返すのかが混在する、紛らわしいバグを避けられる。
// () => Promise<number> と推論される
async function getNumber() {
return 42
}
const getNumber = async () => 42// Promise を直接使った場合
const getNumber = () => Promise.resolve(42)キーワード
- TypeScript
- JavaScript
- Promise
- async/await
- コールバック地獄
- 型推論
- 可読性