TypeScriptの例外処理をEitherでスマートに記述する方法を紹介します。

Table of Contents

はじめに

TypeScriptにはEither型がありません。
通常はUnion Typesを使ってResult | Errorのような表現をします。

一方、関数型言語では同じことを表すのにEither型を使うことが多いです。
Eitherは文脈を持つコードを書けるので、上手く使えばスマートになります。

お題

今回はお題として以下の要件を満たすコードを書き比べてみます。

  • 3つのファイルを読み込んで、中身を結合し標準出力に出力する
  • 読み込みに失敗したファイルは標準エラー出力にエラーメッセージとファイル名を出力する

構成

以下3つのファイルを用意します。

a.txt
a a a
b.txt
b b
c.txt
c

TypeScriptのcompilerOptions.targetes2015にしました。

期待値

例えばa.txtc.txtの読みこみに成功、bb.txtの読みこみに失敗した場合、出力は以下を期待します。

a
a
a
ファイルの読みこみに失敗しました (./bb.txt)
c

標準エラー出力の出力タイミングは同期しないので、出力場所は多少前後します

Union Typesを使うケース

まずはUnion Typesを使うケースから紹介します。

様々な書き方があると思いますが、例えば以下の様に書けます。

main.ts
import { promises } from "fs";

const PATHS = [
    "./a.txt",
    "./bb.txt",
    "./c.txt",
]

const isError = (item: any): item is Error => item instanceof Error

const load = async (path: string): Promise<string | Error> =>
    await promises.readFile(path)
        .then(String)
        .catch(e => new Error(`ファイルの読みこみに失敗しました (${path})`))

async function main() {
    const results = await Promise.all(PATHS.map(load))
    results.map(x => isError(x) ?
        console.error(x.message) :
        console.log(x)
    )
}

main()
ExperimentalWarning: The fs.promises API is experimental
fs.promisesは試験的なAPIであるため表示される警告です。
いきなり仕様が変わる点を許容出来れば大きな問題にはならないと思います。

Eitherを使うケース

次にEitherを使うケースです。
冒頭の通り、TypeScriptにEitherは無いのでfp-tsを使用します。

fp-tsはEitherに限らず、関数型の記載をするために必要な様々な機能が用意されています。

fp-tsのEitherを使うと先ほどのコードは以下の様に書けます。

main.ts
import { TaskEither, tryCatch } from "fp-ts/lib/TaskEither";
import { promises } from "fs";

const PATHS = [
    "./a.txt",
    "./bb.txt",
    "./c.txt",
]

const load = (path: string): TaskEither<Error, string> =>
    tryCatch(
        () => promises.readFile(path).then(String),
        e => new Error(`ファイルの読みこみに失敗しました (${path})`)
    )

async function main() {
    const results = await Promise.all(PATHS.map(x => load(x).run()))
    results.map(x => x.fold(
        e => console.error(e.message),
        console.log
    ))
}

main()

比較

今回の例による違いは、EitherはType Guardsを使うためにErrorを定義する必要がないことだと思います。
とはいえisErrorを定義するだけなので全く手間にはなりません。

一方で受け取った結果を、同様に文脈を考慮して変換し続ける場合はEitherの方が優れていると考えています。
なぜなら、fp-tsは関数型言語で便利な実装をする機能を沢山持っているからです。

総括

TypeScriptの例外処理について、普通の書き方とEitherを使った書き方を紹介しました。

筆者の関数型言語力が不足しているため、Eitherのメリットを見いだすことはあまりできませんでした🙇
もう一度、すごいH本を読んで修行してきます。

その際に新しい気づきがあったら、また記事にしていきたいと思います。