🙅

neverthrowのsafeTryのすゝめ

2024/08/29に公開

はじめに

neverthrowはJavaScript/TypeScriptのResult実装を提供するライブラリです。筆者は2024年6月頃からこのライブラリのメンテナとして活動しています。

neverthrowでは複数のResultを直列に扱うとき、andThenで繋いだり一度エラーハンドリングをするのが基本のスタイルでしたが、v6.1.0から入ったsafeTryを使うとasync関数やHaskellのdo構文のようなノリで書けるようになります。さらに、執筆時点での最新版であるv7.0.1で型定義が改善され、使いやすさが格段に向上しました。safeTry自体があまり知られていないような気もするので、これをいい機会とみなして簡単に紹介したいと思います。

なお、対象読者はneverthrowの基本的なAPIを理解している方となります。それ以外にも、Result/Either型の一般的知識を持っている方は雰囲気でわかると思います。

safeTryの基本

READMEの例を少し手直しして説明していきます。

declare function mayFail1(): Result<number, string>;
declare function mayFail2(): Result<number, string>;

function myFunc(): Result<number, string> {
    const result1 = mayFail1();
    if (result1.isErr()) {
        return err(`aborted by an error from 1st function, ${result1.error}`);
    }
    const value1 = result1.value

    const result2 = mayFail2();
    if (result2.isErr()) {
        return err(`aborted by an error from 2nd function, ${result2.error}`);
    }
    const value2 = result2.value

    return ok(value1 + value2);
}

このコードはmayFail1が成功していたらmyFail2を実行し、2つの成功の場合の値を使って計算を行っています。どちらかが失敗した場合は即座にエラーを返しています。Resultが2つしかないので大したことないですが、これが増えていくとその分だけxxx.isErr()でのチェックが増えていくことになって面倒です。

safeTryを使うとこれを以下のように書き換えられます。

function myFunc(): Result<number, string> {
    return safeTry(function*() {
        const value1 = yield* mayFail1()
                .mapErr(e => `aborted by an error from 1st function, ${e}`)
                .safeUnwrap()
        const value2 = yield* mayFail2()
                .mapErr(e => `aborted by an error from 2nd function, ${e}`)
                .safeUnwrap()

        return ok(value1 + value2)
    })
}

mayFail1がエラーの場合、myFunc自体の戻り値がそのエラー(をmapErrで変換したもの)となり、const value2 = ...以降の行は実行されません。成功の場合はisOkでのチェック無しにvalue1にその値が割り当てられます。value2についても同様です。これだけでasync関数の挙動に似ていることがお分かりいただけるかと思います。

あるいは、1つ目のResultの成功値を使って2つ目のResultを計算したい場合を考えてみましょう。

declare function mayFail1(): Result<number, string>;
declare function mayFail2(n: number): Result<number, string>;

function myFunc(): Result<number, string> {
    return mayFail1()
        .mapErr(e => `aborted by an error from 1st function, ${e}`)
        .andThen(value1 =>
            mayFail2(value1)
                .mapErr(e => `aborted by an error from 2nd function, ${e}`)
        )
}

これは関数型的な書き方なので好きな方も多いでしょうが、チームによっては受け入れが難しいこともあるかも知れません。これもsafeTryを使うとasync関数っぽくなります。

function myFunc(): Result<number, string> {
    return safeTry(function*() {
        const value1 = yield* mayFail1()
                .mapErr(e => `aborted by an error from 1st function, ${e}`)
                .safeUnwrap()
        return yield* mayFail2(value1)
                .mapErr(e => `aborted by an error from 2nd function, ${e}`)
                .safeUnwrap()
    })
}

従来の課題点とv7.0.1での改善

このように便利なsafeTryですが、エラーの型が複数ある場合の使い勝手に難がありました。

declare function mayFail1(): Result<number, 'err1'>;
declare function mayFail2(): Result<number, 'err2'>;

function myFunc(): Result<number, 'err1' | 'err2'> {
    // 型引数を明示しなければならない
    return safeTry<number, 'err1' | 'err2'>(function*() {
        const value1 = yield* mayFail1()
                .safeUnwrap()
        const value2 = yield* mayFail2()
                .safeUnwrap()

        return ok(value1 + value2)
    })
}

mayFail1mayFail2はそれぞれ異なるエラーの型を返します。その場合、safeTryの型引数として成功時とエラー時の型をそれぞれ指定しなければ型検査が通りませんでした。ドキュメンテーションとしての役割もあると考えれば型は明記するに越したことはないですが、少々面倒でした。

それが、ユーザーからのPRにより型定義に変更が加わり、v7.0.1からはこのケースでも型引数の指定が不要になりました👏👏👏これにより、safeTryは留保無しにお勧めできる機能になったと思います。

以上、safeTryのすゝめでした。ここまで読んでいただいて仕組みが気になった方もいらっしゃるかも知れません。内部実装もなかなか賢いことをやっているので本当はそこまで解説したかったのですが、それはまた日を改めて

Discussion