neverthrow の全機能リファレンス
JavaScript/TypeScript で try/catch を使わないエラーハンドリングに利用できるライブラリとしてはそこそこ有名だと思う neverthrow ですがあまり解説された記事が少なく、関数型と手続き型の書き方をいい感じにミックスできるいいライブラリで情報の少なさから選択されないのも勿体なく感じました。ちょうど GitHub のドキュメントを読みながら意訳してまとめた手元の技術メモがありますのでその一助になればと公開します。
neverthrow とは
supermacro/neverthrow: Type-Safe Errors for JS & TypeScript
プログラムのエラーハンドリングを try - catch ではなく関数型プログラミング由来の Result
型や Either
型と呼ばれる方法で実現する機能を提供するライブラリです。具体的には Result
型を提供してくれ Option
型が値の有無を表すように処理の成功/失敗を表してくれます。
例えばバックエンドのリクエストを受けてレスポンスを返すまでの処理フローは、neverthrow
を使うと以下のように記述することが出来ます。途中の処理でなにかエラーが置きた場合、最後のレスポンスを返す処理まで残りの処理はスキップされます。
ok(request)
.andThen((request) => validate(reqest))
.andThen((validatedRequest) => createUser(validatedRequest))
.andThen((record) => toResponse(record))
.match(
(response) => res.status(200).json(response),
(error) => res.status(error.statusCode).json(error.data),
)
突き詰めれば関数型プログラミング的に全ての関数をこのように書くことも出来るのですが、neverthrow
は手続き的に書いたものを Result
に変換するサポートも行ってくれるので、要所に応じて使い分ける事ができます。
似たような事ができるライブラリとしては fp-ts があり、こちらは Either
型だけでなく様々な関数型プログラミングを支援する機能があるライブラリです。本格的に関数型プログラミングに取り組みたい時はこちらの方がおすすめですが、エラーハンドリング周りだけ関数型プログラミングの要素を取り入れたい時には neverthrow
はおすすめです。
インストール
npm install neverthrow
npm install eslint-plugin-neverthrow
mdbetancourt/eslint-plugin-neverthrow
必須ではないけどエラー処理がされていない Result
に対して警告を出してくれます。
API
neverthrow
から export
されている関数や型です。
- Result, ResultAsync
- 成功/失敗を表す型
- 定義は
Result<T, E> = Ok<T, E> | Err<T, E>
( T : 成功値, E : 失敗値 ) となります - ResultAsync は非同期版で
Promise<Result>
を Result と同じように扱えます
- Ok
- 成功を表す型で
value
プロパティに成功値を持つ
- 成功を表す型で
- Err
- 失敗を表す型で
error
プロパティにエラー値を持つ
- 失敗を表す型で
- ok, okAsync
- 引数を
Ok
型にして返す関数
- 引数を
- err, errAsync
- 引数を
Err
型にして返す関数
- 引数を
- fromThrowable
- 例外を投げる関数を
Result
を返す関数にして返す関数
- 例外を投げる関数を
- fromPromise
- 例外を投げる可能性がある
Promise
をResultAsync
に変換する関数
- 例外を投げる可能性がある
- fromSafePromise
- 例外を投げない事がわかっている
Promise
向けのfromPromise
- 例外を投げない事がわかっている
- safeTry
- ボイラープレート的なエラーハンドリングを簡潔な記述にできるユーティリティ関数
fromThrowable, fromPromise, fromSafePromise は Result/ResultAsync の static method に同様のものがあり、使い方は同じなので説明はそちらを参照下さい。
safeTry は Result/ResultAsync の safeUnwrap メソッドと一緒に使う関数で、こちらの使い方も後述しています。
Result API
- Result.isOk
- Ok 値であれば true を返す
- Result.isErr
- Err 値であれば true を返す
後述の unwrapOr
や match
以外でフローの締め処理を行いたい時などに利用できます。
const result = someFunction()
if (result.isOk()) {
// Ok 型が確定する
console.log(result.value)
}
if (result.isErr()) {
// Err 型が確定する
console.log(result.error)
}
Result.map
Result<T, E>
を Result<U, E>
と成功時の値を変換する関数で、Ok 値である場合のみに実行されます。引数にわたす関数の型は (value: T) => U
です。
// map<U>(callback: (value: T) => U): Result<U, E> { ... }
ok("1").map(v => Number(v))
Result.asyncMap
Result.map
の非同期版で Promise を返す関数を引数に取り、戻り値として ResultAsync を返します。
Result.mapErr
Result<T, E>
を Result<T, F>
と失敗時の値を変換する関数で、Err 値である場合のみに実行されます。引数にわたす関数の型は (value: E) => F
です。
// mapErr<F>(callback: (error: E) => F): Result<T, F> { ... }
err("error").mapErr(error => log.Fatal(error))
Result.andThen
基本的な考えは Result.map
と同じで Ok 値の場合のみ実行される関数です。map
とは違って引数に渡す関数が値ではなく Result
を返す必要があります。引数の関数を型にすると (value: T) => Result<U, F>
です。
// andThen<U, F>(callback: (value: T) => Result<U, F>): Result<U, E | F> { ... }
const double = v => ok(v + v)
ok(2).andThen(double).andThen(double) // Ok(8)
err(2).andThen(err).andThen(double) // Err(2)
Result.asyncAndThen
Result.andThen
と同様の機能の非同期版で ResultAsync
を返す関数を引数に取ります。処理途中から非同期処理を行いたい場合に利用できます。
Result.orElse
Result.andThen
のエラー処理版のような関数で Err 値の場合にのみ実行される関数です。新しい Result
を返す事ができるので、特定のエラーの場合にはリカバリー処理を行って処理を続行させる事もできます。
// orElse<A>(callback: (error: E) => Result<T, A>): Result<T, A> { ... }
queryError.orElse(error => {
error === QueryError.NotFound
? ok('user not found') // 処理は成功したが見つからなかったとして処理する
: err(500) // internal server error として処理する
})
Result.unwrapOr
Ok
値であればその値を、Err
値であれば引数に渡したデフォルト値を返す関数です。
// unwrapOr<T>(value: T): T { ... }
ok(2).unwrapOr(10) // -> 2
err("error").unwrapOr(10) // -> 10
Result.match
Ok
値に対する関数と Err
値に対する関数の 2 つの引数をとり、実際の値が一致する方の関数が実行されます。それぞれの関数で結果を返すこともできますが、この時の戻り値の型は共通のものにする必要があります。また、match
の戻り値は Result
型ではなく返した値の型です。
class Result<T, E> {
match<A>(
okCallback: (value: T) => A,
errorCallback: (error: E) => A,
): A => { ... }
}
Result.fromThrowable (static method)
例外を投げる可能性がある関数と投げられた例外を変換する関数の 2 つを引数に取り、結果を Result で返す関数に変換してくれる関数です。例外を投げる外部のライブラリを neverthrow を使った処理フローが行えるようにしたい場合に利用します。
const safeJsonParse = Result.fromThrowable(JSON.parse, toParseError)
// 元の関数と同じように使えるが結果は Result として返る
const result = safeJsonParse(jsonString)
同様の関数が通常の関数としても定義されている。
import { fromThrowable } from 'neverthrow'
Result.combine (static method)
Promise.all のように Result のリストを 1 つの Result にまとめる関数です。成功や失敗の値の型が異なっていてもまとめる事はできるが、Result と ResultAsync を混ぜる事はできません。また、渡すリストは配列だけでなくタプルも渡せます。
combine は引数に渡したリストの全ての Result が Ok 値の場合はそれらの値のリストを持った Ok を返し、Err 値が含まれている場合は最初に見つかった Err を結果として返します。
結果の型が異なる配列の場合はユニオン型の配列が結果の型となります。
const arr = [ok(1), ok('ok')] // (Result<number, never> | Result<string, never>)[]
const combinedArr = Result.combine(arr) // Result<(string | number)[], never>
combine に渡す際に配列化して渡そうとするとタプルとして扱われます。変数として渡したい場合は変数の型を明示的に書く方法もありますが、タプルを返すユーティリティ関数を用意すると型推論により型の変更に対しても柔軟に対応できます。
Result.combine([ok(1), ok('ok')]) // Result<[number, string], never>
const tuple = <T extends any[]>(...args: T): T => args
const resultTuple = tuple(ok(1), ok('ok'))
Result.combine(resultTuple) // 上記と同じ結果の型となる
Result.combineWithAllErrors (static method)
Result.combine と同じく結果をまとめる関数ですが、リストに Err 値が複数含まれていた場合に全ての Err をリストとして返します。
Result.combineWithAllErrors([ok(1), err(2), err(3)]) // Err([2, 3])
ResultAsync API
基本的に Result API と同様のものが使えるので特有のものだけ紹介します。
ResultAsync.fromPromise (static method)
例外を投げる可能性がある Promise
を ResultAsync
に変換する関数です。具体的には内部で Promise
の then
及び catch
が処理され結果を ResultAsync
にして返します。
第 2 引数には投げられた例外を変換する関数を渡す必要があります。後続の処理で扱いやすいようにオブジェクトに変換することが推奨されています。例外を投げないことがわかっている場合は冗長になるので、後述する fromSafePromise
を使えば第 2 引数を省略できます。
ResultAsync.fromPromise<T, E>(
promise: PromiseLike<T>,
errorHandler: (unknownError: unknow) => E)
): ResultAsync<T, E> { ... }
ResultAsync.fromPromise(insertUser(request), () => new Error('database error'))
ResultAsync.fromSafePromise (static method)
ResultAsync.fromPromise
と同様に Promise
を ResultAsync
に変換する関数です。fromPromise
とは違い Promise
が例外を投げないことがわかっている時に利用します。この関数内では例外が投げられても Promise
の catch
が処理されないので注意が必要です。
safeTry
Result
を返す関数の中で複数の Result
を返す関数を呼び出し、それらの結果の値を取り出して処理する必要がある場合、通常は以下のようなコードになります。
const example = (): Result<number, string> => {
const result1 = foo()
if (result1.isErr()) {
return err(`aborted: ${result1.error}`)
}
const result2 = bar()
if (result2.isErr()) {
return err(`aborted: ${result2.error}`)
}
return ok(result1.value + result2.value)
}
このボイラープレート的な手続きを safeTry
と Result/ResultAsync
の safeUnwrap
を利用する事で以下のように書くことができます。(ResultAsync
の場合は yield*
の後に await
が必要になります)
const example = (): Result<number, string> => {
return safeTry<number, string>(function* () {
const result1 = yield* foo().mapErr(e => `aborted: $(e)`).safeUnwrap()
const result2 = yield* bar().mapErr(e => `aborted: $(e)`).safeUnwrap()
return ok(result1 + result2)
})
}
ジェネレーターと safeUnwrap
の工夫により、処理の途中で Err
を返した関数があればその時点で中断し、 safeTry
の結果は上記の例だと mapErr
の値となり、全ての関数が Ok
を返した場合は最後まで処理された値が返ります。
公式のサンプルだと関数型言語に近い書き方に一気に修正されているので、人によっては可読性の悪いコードに見えたので抵抗があったのですが、上記のように if
による分岐とは異なるエラーハンドリングの形式が提供されていると理解できれば良い機能だと感じました。
Testing API
Result
のインスタンスは比較可能なので通常通りテストすることが出来ます。
expect(result).toEqual(ok(value))
テスト用に以下のような関数も用意されています。
-
Result._unsafeUnwrap
- Ok 値なら結果を返し、Err 値ならカスタムオブジェクトを返す
-
Result._unsafeUnwrapErr
- Err 値ならエラーを返し、Ok 値ならカスタムオブジェクトを返す
また、これらは通常はスタックトレースを含みませんがオプションを渡すことによりスタックトレースを生成する事も出来ます。
_unsafeUnwrap({ withStackTrace: true })
参考
- 公式が紹介している neverthrow を使った実践的なサンプル
- neverthrow を使ってバックエンドを開発する際の考え方が解説されている
- 関数型プログラミングの考え方の基礎の一部を学べる
Discussion