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