TreeShakableなResultライブラリを作りました
はじめに
JavaScriptでは、throwを使ってエラーを明示的に投げることで、処理を中断する「大域脱出」が可能です。しかし、TypeScriptではこのthrowによって発生するエラーの型を記述できないため、型安全性が損なわれてしまいます。
この問題を解決するために、関数の成功・失敗を明示的に扱えるResult型が有用です。
TypeScriptでResult型を利用する場合、neverthrowやeffect-ts、fp-tsなどのライブラリがよく挙げられます。
しかし、それぞれ一長一短があり、neverthrowは比較的シンプルで使いやすいものの、現在は活発なメンテナンスが行われておらず、複数のPRが長期間放置されています。
effect-tsやfp-tsは高機能ですが、Result以外の多くの概念も含んでいるため、「Resultだけ欲しい」ケースでは導入コストが高く、バンドルサイズも膨らみがちです。
そこで、これらの課題を解決すべく、Result型に特化したシンプルでTreeShakableなライブラリ @praha/byethrowを開発しました。
@praha/byethrowの特徴
クラス非依存で実態はシンプルなオブジェクト
@praha/byethrowはResultをclassではなく、シリアライズ可能なただのオブジェクトとして 表現しています。
そのため、ResultをそのままJSONに変換する事が可能であり、RSC(React Server Components)のServerActionsの戻り値として利用する事ができます。
サーバーからResultを返し、クライアント側で@praha/byethrowの関数群を利用してResultを操作するといった事が可能になります。
TreeShaking対応
@praha/byethrowには幾つかの関数群がResultまたは、省略記法であるRというネームスペースから参照することが出来ます。
import { R } from '@praha/byethrow';
const input = R.succeed(2);
const result = R.pipe(
input,
R.map((value) => value * 3),
);
このネームスペースはre-exportを使用して実現しているためTreeShaking可能です。ViteやWebpackなどのバンドラーを利用することで利用していない関数は自動的にバンドルから除外することが出来ます。
非同期でも同じ関数が使える
@praha/byethrowは非同期のResult(ResultAsync)でも同期と同じAPIを利用可能です。
たとえば、neverthrowではasyncMapやasyncAndThenといった専用の関数が必要でしたが、本ライブラリではmapやandThenをそのまま使うことができます。
import { R } from '@praha/byethrow';
const result1: R.Result<number, string> = R.pipe(
R.succeed(2),
R.andThen((value) => {
if (value <= 0) {
return R.fail('Value must be greater than 0');
}
return R.succeed(value * 3);
}),
);
const result2: R.ResultAsync<Response, string> = R.pipe(
R.succeed('https://example.com'),
R.andThen((url) => {
if (!url.startsWith('https')) {
return R.fail('The URL must begin with https');
}
return R.succeed(fetch(url));
}),
);
これにより同期・非同期を意識せず同じインターフェースで書けるため、直感的で統一感のあるコードが書けます。
充実したドキュメント
全ての関数にTSdocで使用例付きのドキュメントを記述しており、APIリファレンスページも公開しており、ライブラリの初学者でも迷わず使えるよう配慮しています。

andThenのドキュメントを一部抜粋
今後はより実践的な例として、簡単なAPIサーバーを模したコードサンプルも追加予定です。
そもそもResultは必要なのか?
「JavaScriptはどこでthrowされるか分からないから、全部をResultで管理するのは無理だからResultを導入する意味は無い」といった意見を見かけることがあります。
しかし、私はこの意見に必ずしも賛同しません。
なぜならResultで扱うべきは、「想定しうるエラー」だけで十分であり、全てのエラーをResultで扱う必要はないためです。
例えば投稿削除APIにおいて:
- 既に削除されています
- 削除権限がありません
といった失敗はアプリケーションレベルで処理すべきもので、Resultによるハンドリングが適しています。一方で、データベース接続エラーや未知の例外はthrowしてSentryなどのインフラレベルで捕捉すれば良く、すべてをResultに包む必要はありません。
おわりに
@praha/byethrowは、「型安全なエラーハンドリングをシンプルに実現したい」という方にちょうどいい立ち位置を目指して設計しています。
neverthrowでは物足りないけどeffect-tsやfp-tsは重すぎる。
そんな方は、ぜひ一度@praha/byethrowを試してみてください。
良いなと思った方ははぜひリポジトリにスターを付けていただけると嬉しいです!
Discussion