はじめに
この投稿は、TypeScript学習者が書いています。関数の引数として() => Promise<Response>を受け取る書き方と、Promise<Response>を受け取る書き方の違いを、自身の理解のために整理します。
例として、fetchをtry/catchで包む関数を書く場面を考えます。引数の型がどう違うのか、いつ処理が始まるのかを見ていきます。
fetch("/example")をそのまま渡す場合と、アロー関数で渡す場合は何が違うのか、型エラーの理由、引数をPromise<Response>にした場合、実行タイミングの違いを比較します。
1. () => Promise<Response>とPromise<Response>
同じfetch("/example")を扱うとき、次の2つは別物です。
const run: () => Promise<Response> = () => fetch("/example");
const promise: Promise<Response> = fetch("/example");
| 書き方 | 型 | いつ処理が始まるか |
|---|---|---|
() => fetch(url) |
() => Promise<Response> |
run()を呼んだとき |
fetch(url) |
Promise<Response> |
その行を評価したとき |
run: () => Promise<Response>は関数の型で、あとから実行する関数を受け取ります。fetch(url)を引数に直接書くと、引数を評価した時点でfetchが実行されます。
2. () => Promise<Response>を受け取るとき
次のcatchNetworkErrorは、fetchをtry/catchで包む関数です。tryの中でawait run()し、必要なのはPromise<Response>そのものではなく、呼ぶとPromise<Response>が返る関数です。
declare function fetch(input: string): Promise<Response>;
async function catchNetworkError(
run: () => Promise<Response>,
): Promise<Response> {
try {
return await run();
} catch {
throw new Error("失敗しました");
}
}
catchNetworkError(() => fetch("/example")); // OK
catchNetworkError(fetch("/example")); // 型エラー
// Argument of type 'Promise<Response>' is not assignable to parameter of type '() => Promise<Response>'.
// Type 'Promise<Response>' provides no match for the signature '(): Promise<Response>'.
fetch("/example")の型はPromise<Response>です。() => Promise<Response>には代入できません。アロー関数で渡すと、() => fetch("/example")全体が関数の型になり、run()の時点でfetchが実行されます。
3. Promise<Response>を受け取るとき
引数の型をPromise<Response>に変えれば、fetch(url)をそのまま渡せます。
async function catchNetworkError(
promise: Promise<Response>,
): Promise<Response> {
try {
return await promise;
} catch {
throw new Error("失敗しました");
}
}
catchNetworkError(fetch("/example"));
fetch("/example")は呼び出しの前に引数の式として評価され、その時点で返ったPromiseがcatchNetworkErrorに入ります。catchNetworkErrorの中ではtryでawait promiseしているので、失敗してrejectされてもcatchで拾えます。型の違いは、いつfetchが実行されるかです。
実行順の違い
Promiseを渡すと、ラッパーに入る前にfetchが走ります。アロー関数を渡すと、ラッパーの中でrun()が呼ばれたときにfetchが走ります。
const log: string[] = [];
function trackFetch(): Promise<Response> {
log.push("fetch");
return Promise.resolve({} as Response);
}
async function wrapPromise(promise: Promise<Response>) {
log.push("enter");
return promise;
}
async function wrapRun(run: () => Promise<Response>) {
log.push("enter");
return run();
}
async function demoOrder() {
log.length = 0;
await wrapPromise(trackFetch());
console.log(log); // ["fetch", "enter"]
log.length = 0;
await wrapRun(() => trackFetch());
console.log(log); // ["enter", "fetch"]
}
demoOrder();
同じ処理を何度も実行したいとき
リトライのように、同じfetchを何度も実行したい場面では、() => Promise<Response>で渡す必要があります。Promiseは1回だけ、run()は呼ぶたびに実行されます。
let count = 0;
function trackFetch(): Promise<Response> {
count += 1;
console.log("fetch", count);
return Promise.resolve({} as Response);
}
async function demoCount() {
count = 0;
const promise = trackFetch();
await promise;
await promise;
console.log("promise版", count); // fetch 1 → promise版 1
count = 0;
const run = () => trackFetch();
await run();
await run();
console.log("run版", count);
// fetch 1
// fetch 2
// run版 2
}
demoCount();
まとめ
- 引数の型が
() => Promise<Response>のとき、fetch(...)そのもの(型はPromise<Response>)はそのまま渡せません。呼ぶとfetchが始まる関数を渡します。アロー関数でも関数宣言でも構いません。 - 引数を
Promise<Response>にすればfetch(...)を直接渡せます。tryでawaitすればrejectも拾えます。 - 引数の評価時ではなく、受け取る関数の中で実行したいときや、同じ処理を何度も実行したいときは、
() => Promise<Response>を使います。