こちらの記事は2021年度の「株式会社カケハシ x TypeScript」アドベントカレンダーの2日目の記事になります。
1日目の記事からの続きになります。
今回はPromiseとEitherの組み合わせを具体的なユースケースに落とし込んで説明していきます。説明のために定義を用意します。以下は、BillingRequesterは請求を送信するためのインターフェースです。
以下は、請求の成功可否をアカウントIDに紐付けた独自型です。
以下は、BillingRequester#postを呼び出して、成功可否を元にBillingResult型に変換をしています。このようにしてPromise結果を変換することができ、Promise<T>のTをUnion型で持つことで成功と失敗の型を表現できます。サンプルコードでは全ての異常系をcatchしているのでPromiseは全て成功となります。
そもそもPromiseは非同期を抽象化したデータ型ですが、Either同様に異常系の型を含むことが可能です。ただ、BillingResult(Union型)については基礎編で取り上げたようにEither化することで関数合成がしやすくしていきます。とはいえ、Promise<Either<E,A>>はがネストしていてPromiseとEitherとでそれぞれのデータ型の処理を行う必要が出てきます。(これはEitherを導入したから発生した課題ではなく、Promise<Union型>でも同様の課題です。)
さて、PromiseとEitherを取りまとめて扱える関数があると便利だと思いませんか。fp-tsではTaskとTaskEitherというデータ型が用意されています。
Task / TaskEitherとは
Taskの型定義は、<A>() => Promise<A>です。Taskは異常を起こさないPromiseとして扱います。
TaskEitherは、Task<Either<E, A>>のことを表します。TaskEitherを使うことで複数のPromiseの異常系をシンプルに扱うことができます。Promiseの値をEitherにしたデータ型で、Eitherと同じようにmap/flatten/concat/applyのような高階関数が利用できます。実際の利用の仕方は、fp-ts recipes#Async Tasks で紹介されています。また、日本語の記事では以下の記事が参考になりました。
では、実際に具体的なユースケースを実装する上で、Promise<Union>版とTaskEither版のコードを書いて比較していきます。
[ユースケース]
- 複数のアカウントに請求を送信する
- 他のアカウントの保存に失敗に関わらず非同期処理を実行する
- アカウント毎に成功可否の値を使いたい
Promise<Union>版
PromiseSettledResultはPromise#allSettledをした時にPromiseの成功・失敗の結果をUnion型で持ちます。lib.es2020.promise.d.tsの定義は以下です。
reasonがanyであるため、固有のアカウントIDにエラー情報を型安全にマッピングすることができません(instanceofをしなくてはならなりません)。そこでBillingServiceWithPrimiseUnion#requestの関数内では型安全にBillingErrorを扱うためにtry-catchしてPromiseSettledResult型はPromiseFulfilledResult型にしかならないように工夫しています。
以下のseparatePromiseResult関数では、BillingServiceWithPrimiseUnion#bulkPostで受け取った型から成功の配列と失敗の配列に分割しています。
PromiseUnion版だとクライアント関数での取り扱いが少々複雑な分岐が発生しています。
TaskEither版
- 前述のようなBillingResultの具象の構造に依存せずに処理系(今回の場合はseparateResult)を書くことができます
- 型安全にするためのHackが不要です
Promise<Union>版で使っていたBillingResultは不要です。サンプルコードでは独自エラー型のBillingErrorだけを定義しました。
A.sequence(T.ApplicativePar)が何をやっているか分からないかもしれませんが、fp-ts recipes#Comparison with Promise methodsにてPromiseメソッドとの対応表が記載されており、実現したいことに合わせて規定の呼び出し方をすれば問題ありません。
Promise<Union版>のseparatePromiseResultが使うと以下のように簡潔に実装できます。
どうでしょうか。TaskEither版の方がシンプルにかつ型安全に実装できているかと思います。
まとめ
TypeScriptでEitherを使う時に、ほとんどのケースでTaskEitherも使うことになります。複数のPromiseを型安全かつシンプルな処理を書く上でTaskEitherを使う選択肢も検討してみてはいかがでしょうか。
3日目は@hedrall さんのesbuildの記事です。個人的にも楽しみです。