neverthrowのsafeTryのすゝめ
はじめに
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)
})
}
mayFail1
とmayFail2
はそれぞれ異なるエラーの型を返します。その場合、safeTry
の型引数として成功時とエラー時の型をそれぞれ指定しなければ型検査が通りませんでした。ドキュメンテーションとしての役割もあると考えれば型は明記するに越したことはないですが、少々面倒でした。
それが、ユーザーからのPRにより型定義に変更が加わり、v7.0.1からはこのケースでも型引数の指定が不要になりました👏👏👏これにより、safeTry
は留保無しにお勧めできる機能になったと思います。
以上、safeTry
のすゝめでした。ここまで読んでいただいて仕組みが気になった方もいらっしゃるかも知れません。内部実装もなかなか賢いことをやっているので本当はそこまで解説したかったのですが、それはまた日を改めて
Discussion