better-result-adopt について
better-result Adopt は、既存のコードベースに better-result を段階的に導入するスキルです。型付きエラー処理と範囲を絞った移行を案内し、全面的な書き換えを避けます。
主な機能
- 01try/catch を Result API に置換
- 02null 可能な戻り値を Result に変更
- 03ドメイン固有の TaggedError 型を定義
- 04andThen や Result.gen で処理を合成
- 05移行時にエラーのコンテキストを保持
活用シーン
- 01I/O 境界を Result に移行
- 02モジュール内の null による代替値を置換
- 03サービス全体のエラー型を統一
- 04ネストしたエラー処理をリファクタリング
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ dmmulroy/ better-result --skill better-result-adoptインストール後、Claude Code で/better-result-adoptを入力して読み込まれたか確認します。
同梱ファイルreferences/tagged-errors.md
詳細
better-result の導入
既存のコードベースを一度にすべて書き換えるのではなく、better-result を段階的に導入します。
使用する場面
ユーザーが次のことを希望する場合に、このスキルを使用します。
- try/catch から
Result.tryまたはResult.tryPromiseに移行する - nullable な戻り値を型付きの
Result<T, E>に置き換える - ドメイン固有の
TaggedError型を定義する - ネストしたエラー処理を
andThenチェーンまたはResult.genにリファクタリングする - サービスやモジュール全体でエラー処理を標準化する
読む順序
| 作業 | 読むファイル |
|---|---|
| モジュールに better-result を導入する | このファイル |
| エラー型を定義またはレビューする | references/tagged-errors.md |
| ライブラリの実装詳細を確認する | 存在する場合は opensrc/ |
前提条件
コードを編集する前に、次のことを確認します。
- 対象のプロジェクトに
better-resultがすでにインストールされていることを確認します。 opensrc/ディレクトリがあるか確認します。存在する場合は、現在のパターンを把握するため、そこにあるパッケージのソースを読みます。- まず移行範囲を特定します。1 つのファイル、1 つのモジュール、または 1 つの境界レイヤーを対象にします。
移行戦略
1. 境界から始める
I/O 境界や例外処理の多いコードから始めます。
- HTTP クライアント
- データベースアクセス
- ファイルシステム操作
- パースと検証
- フレームワークアダプター
コードベース全体を一度に変換しないでください。
2. 既存のエラーを分類する
| カテゴリー | 例 | 移行後の形 |
|---|---|---|
| ドメインエラー | 見つからない、検証、認証 | TaggedError + Result.err |
| インフラストラクチャエラー | ネットワーク、DB、ファイル I/O | Result.tryPromise + マッピングしたエラー |
| プログラマーの不具合 | 誤った前提、null 参照 | throw のままにします。不具合は Result コールバック内で Panic になります |
3. 次の順序で移行する
- エラー型を定義します。
- throw する境界を
Result.try/Result.tryPromiseでラップします。 - null またはブール値のセンチネル戻り値を
Resultに置き換えます。 - 呼び出し元をリファクタリングし、
Result値を伝播させます。 - ネストした分岐を
andThen、mapError、またはResult.genにまとめます。
主な変換
Try/catch → Result.try
function parseConfig(json: string): Result<Config, ParseError> {
return Result.try({
try: () => JSON.parse(json) as Config,
catch: (cause) => new ParseError({ cause, message: `Parse failed: ${cause}` }),
});
}
非同期の throw → Result.tryPromise
async function fetchUser(id: string): Promise<Result<User, ApiError | UnhandledException>> {
return Result.tryPromise({
try: async () => {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new ApiError({ status: res.status, message: `API ${res.status}` });
return res.json() as Promise<User>;
},
catch: (cause) => (cause instanceof ApiError ? cause : new UnhandledException({ cause })),
});
}
null センチネル → Result
function findUser(id: string): Result<User, NotFoundError> {
const user = users.find((candidate) => candidate.id === id);
return user
? Result.ok(user)
: Result.err(new NotFoundError({ id, message: `User ${id} not found` }));
}
ネストしたフロー → Result.gen
async function processOrder(orderId: string): Promise<Result<OrderResult, OrderError>> {
return Result.gen(async function* () {
const order = yield* Result.await(fetchOrder(orderId));
const validated = yield* validateOrder(order);
const result = yield* Result.await(submitOrder(validated));
return Result.ok(result);
});
}
実行ワークフロー
- 対象モジュールを調べ、
try、catch、.catch(...)、throw、null、undefined、ステータスフラグによるエラー処理を確認します。 - 制御フローを変更する前に、
TaggedErrorクラスを定義または更新します。 - まず境界関数を変換し、シグネチャを
Result<T, E>またはPromise<Result<T, E>>に変更します。 - 直近の呼び出し元を更新し、新しい
Resultを処理または伝播させます。 Resultを返す複数のステップを合成する場合は、Result.genまたはandThenを使用します。cause、ID、メッセージ、その他の構造化フィールドを保持し、エラーのコンテキストを維持します。- テストを実行し、成功パスとエラーパスの両方のカバレッジを追加します。
完了条件
次の条件を満たすと、移行は完了です。
- 対象の関数が、想定されるドメインエラーに対して try/catch に依存しなくなっている
- nullable な戻り値やセンチネルエラー戻り値が、明示的な
Result値に置き換えられている - ドメインエラーに型付きの
TaggedErrorクラスを使用している - 呼び出し元が
Resultを伝播するか、明示的にアンラップまたはパターンマッチしている - テストで少なくとも 1 つの成功パスと、代表的なエラーパスがカバーされている
よくある落とし穴
- 境界から始めず、あらゆる処理を過剰にラップする
- エラーをマッピングする際に、元の失敗のコンテキストを失う
- 同じフローの深い部分で throw ベースと
Resultベースの API を混在させる - 根本原因の不具合を修正せずに
Panicを捕捉する
このリファレンスの内容
| ファイル | 目的 |
|---|---|
references/tagged-errors.md | TaggedError のパターン、照合、型ガード、例 |
opensrc/ が存在する場合は、実装の詳細と現在の API の動作について、その内容を信頼できる情報源として扱ってください。
コメント
Markdown 対応