こんなことがあった
- あるAPIにアクセスすると本日執筆された記事IDの一覧が帰ってくる
- その記事IDを使って記事の内容を取得できるAPIにアクセスして、記事を取得したい
とりあえず実装する
前提: 以下で登場するインスタンス、メソッドには架空のものがあります。このようなことがしたいのだろう。程度に解釈してください。
何も考えず実装するとこのようになった。
const fetchEntryIds = (): Promise<Array<string>> => {
return request.get('xxxxx/entries/new');
};
const fetchEntry = (entryId: string): Promise<Entry> => {
return request.get(`xxxxx/entries/${entryId}`);
};
const fetchTodaysEntries = async (): Promise<Array<Entry>> => {
const entryIds: Array<string> = await fetchEntryIds();
return Promise.all<Entry>(entryIds.map<Promise<Entry>>((entryId: string) => {
return fetchEntry(entryId);
}));
};
期待したこと
Promise.all()
はArray<Promise<?>>
をPromise<Array<?>>
にしてくれる便利なメソッドであり、内部のPromise<?>
の動作を並列に行ってくれる。
-> 記事をまとめて取得できるのでは!?
👶今回起きたこと
- 当たり前だがAPIリクエストは失敗することがある
-
Promise.all()
は内包するすべてのPromiseがfulfilledにならないとfulfilledにならない
そのためリクエストがひとつでもrejectedになってしまうとPromise.all()
はrejectedになり、その値が返却される。悲しいことにそれまでいくつかfulfilledになっているPromiseがあったとしてもなかったことになる。
図解
以下の記号を使います。
- pending = 👶
- fulfilled = 🤖
- rejected = 🧟
以下は左辺にこれらの絵文字を配列状に筆記したもので、右辺はそれをPromise.all()
で実行した場合の結果です。
[👶, 👶, 👶, 👶, 👶] => 👶
[👶, 👶, 🤖, 👶, 👶] => 👶
[👶, 🤖, 🤖, 👶, 🤖] => 👶
[🤖, 🤖, 🤖, 🤖, 🤖] => 🤖
[👶, 👶, 👶, 🧟, 👶] => 🧟
[👶, 🧟, 👶, 🧟, 👶] => 🧟
[🧟, 👶, 👶, 🤖, 👶] => 🧟
[🧟, 🤖, 🤖, 🤖, 🤖] => 🧟
今回やりたいこと
失敗でいいからPromise.all()
の動作を遮らずにすべて実行してほしい。
"実装": "0.1.0"
今回の実装は外部パッケージを一切使わずにできました。
あとで見返せば当たり前じゃないかって思いますが、思いついたときは若干感動しました。
const fetchTodaysEntries = async (): Promise<Array<void | Entry>> => {
const entryIds: Array<string> = await fetchEntryIds();
return Promise.all<void | Entry>(entryIds.map<Promise<void | Entry>>((entryId: string) => {
return fetchEntry(entryId).catch(() => {});
}));
};
こつ
一度rejectedになったPromiseをfulfilledに戻します。そのためにpromise.catch()
で例外を捕捉し、その捕捉にしかるべき処理を施します。今回は握り潰しましたごめん。
問題点
promise.catch()
の戻り値が記事のクラスEntry
ではないためPromise.all()
の戻り値のジェネリクスが<Entry>
から<void | Entry>
に変わりました。fetchTodaysEntries()
を使う人はその戻り値についてundefined
かどうかの判定が必要です。
他にNull Object patternや番兵を使って動作を制御させる方法もありますが、以下はそれよりもより良いだろう方法の紹介です。
"実装": "1.0.0"
Discriminated unions
を使います。
Discriminated unions
複数の型が同名のプロパティを持ち、そのプロパティの型が違うことがわかっていれば、それらから構成されるUnion type
はそのプロパティの型が何かを判定できればTypeScriptはUnion type
がどの型であるかを確定できます。
よくある例
人気No.1モナドのOptional<T>
を部分的に実装する例が最も有名です。
type Some<T> = {
present: true;
value: T;
};
type None = {
present: false;
};
type Optional<T> = Some<T> | None;
このOptional<T>
において複数の型が同名のプロパティを持ち、そのプロパティ型が違うことがわかっていればとはpresent
です。Some<T>, None
はpresent
において異なるLiteral type
を持っているので(boolean
ではなくtrue, false
であることに注目してください)この値の判定で変数がSome<T>
かNone
のどちらかであることを確定できます。
if (optional.present) {
// TypeScriptはoptional = Some<T>と解釈している
optional.value // 🤗
}
else {
// TypeScriptはoptional = Noneと解釈しているため以下はできない
optional.value // 🙄
}
今回用意したDiscriminated unions
今回はTry<T>
のモナドを模しました。
type Success<T> = {
success: true;
value: T;
};
type Failure = {
success: false;
err: unknown;
};
type Try<T> = Success<T> | Failure;
あとはこれに沿うように成功時はSuccess<T>
、例外発生時はFailure
のオブジェクトリテラルで包みます。
これが1.0.0
です。
const fetchTodaysEntries = async (): Promise<Array<Try<Entry>>> => {
const entryIds: Array<string> = await fetchEntryIds();
return Promise.all<Try<Entry>>(entryIds.map<Promise<Try<Entry>>>((entryId: string) => {
return fetchEntry(entryId).then((entry: Entry) => {
return {
success: true,
value: entry
};
}, (err: unknown) => {
return {
success: false,
err
};
});
}));
};
promise.then()
使用例が珍しいですがpromise.then()
は第2引数を受けることができます。この第2引数のコールバックは例外発生時のことで、じつはpromise.catch()
の第1引数と同じです。そのため正常時と例外発生時をpromise.then()
ひとつで処理できます。
もちろんpromise.catch()
を使う方法も可能ですが、オブジェクト生成コストが内部的には少し増えます。気持ちの問題程度の差だとは思うので読みやすさを考慮して適宜選択してください。
const fetchTodaysEntries = async (): Promise<Array<Try<Entry>>> => {
const entryIds: Array<string> = await fetchEntryIds();
return Promise.all<Try<Entry>>(entryIds.map<Promise<Try<Entry>>>((entryId: string) => {
return fetchEntry(entryId).then((entry: Entry) => {
return {
success: true,
value: entry
};
}).catch((err: unknown) => {
return {
success: false,
err
};
});
}));
};
👶async/awaitでやりたい!
できます。手を加えなければいけないところが少々変わります。
fetchEntry()
const fetchEntry = async (entryId: string): Promise<Try<Entry>> => {
try {
const entry: Entry = await request.get(`xxxxx/entries/${entryId}`);
return {
success: true,
value: entry
};
}
catch (err: unknown) {
return {
success: false,
err
};
}
};
array.filter()
できれいにできる
Type predicate
を戻り値に持つarray.filter()
がTypeScriptのArrayにはoverloadで定義されているのでSuccess<T>
だけ、Failure
だけが必要ならそれを使います。
const extractSuccess = (arr: Array<Try<Entry>>): Array<Success<Entry>> => {
return arr.filter((t: Try<Entry>): t is Success<Entry> => {
return t.success;
});
};
const extractFailure = (arr: Array<Try<Entry>>): Array<Failure> => {
return arr.filter((t: Try<Entry>): t is Failure => {
return !t.success;
});
};