第18章:Resultの使い勝手を上げる(ヘルパー)🪄🙂✨
Result型を入れたのに、こんな気持ちになってない?🥺
「成功/失敗が見えるのは最高!…でも if (isErr) が増えてコードが長い〜!😭」
この章は、そのモヤモヤを解決する章だよ💪🌸 **Resultを“いい感じに変換&合成&最後に取り出す”ためのヘルパー(map / mapErr / andThen / match など)**を作って、ネストと分岐をスッキリさせるよ〜🧹✨
0. この章でできるようになること🎯💖
- Resultを 変換できる:
map✅ - エラーだけを 変換できる:
mapErr✅ - 失敗するかもな処理を つなげる:
andThen✅ - 最後に読みやすく 分岐して取り出す:
match✅ - “うっかりthrow”を Resultに閉じ込める:
tryCatch✅ - 3段階の処理を Result でつないで ネストしないコードにできる⛓️✨
1. まず、ヘルパーの役割を3つに分けるね🧠🗂️
Resultヘルパーはだいたいこの3カテゴリに分かれるよ〜👇
- **変換(中身を加工)**🍳
map:Okのvalueだけ変換mapErr:Errのerrorだけ変換
- 合成(処理をつなぐ)⛓️
andThen:次の処理もResultを返すときに使う(ネスト防止✨)
- **消費(最後に取り出す)**📦➡️🎁
match:Ok/Errをきれいに分岐unwrapOr:失敗ならデフォ値で救う
(+おまけ)
tap / tapErr:ログとか副作用だけ挟む🪵tryCatch:throwをResultに変換🧯
実は、人気の Result ライブラリ neverthrow もこの系統のメソッドを揃えてるよ(map / mapErr / unwrapOr / andThen / match など)🧰✨ (GitHub)
2. “最小で強い” Result ヘルパーを作ろう🧰✨
ここでは 依存なしで学習できるように、自分たちの result.ts を用意するよ🙂
(あとで本番では neverthrow 等に置き換えてもOK🙆♀️)
![Resultヘルパーの道具箱[(./picture/err_model_ts_study_018_helper_toolbox.png)
2-1. Result型(復習)📦
// result.ts
export type Ok<T> = { ok: true; value: T };
export type Err<E> = { ok: false; error: E };
export type Result<T, E> = Ok<T> | Err<E>;
export const ok = <T>(value: T): Ok<T> => ({ ok: true, value });
export const err = <E>(error: E): Err<E> => ({ ok: false, error });
export const isOk = <T, E>(r: Result<T, E>): r is Ok<T> => r.ok;
export const isErr = <T, E>(r: Result<T, E>): r is Err<E> => !r.ok;
3. map:Okのときだけ value を変換する🍳✨
3-1. 何が嬉しいの?🥰
if (isOk)を毎回書かなくてよくなる- “成功したときの変換だけ” を安全に書ける
export const map =
<T, U>(f: (value: T) => U) =>
<E>(r: Result<T, E>): Result<U, E> =>
isOk(r) ? ok(f(r.value)) : r;
3-2. 例:数値を文字列にする🧁
const r1: Result<number, string> = ok(3);
const r2 = map((n: number) => `合計は ${n} 円だよ✨`)(r1);
// -> Ok("合計は 3 円だよ✨")
4. mapErr:Errのときだけ error を変換する🧯✨
4-1. どこで使うの?🧐
- エラー型を 統一したい(例:DomainError → AppError)
- 文脈を足したい(例:
"支払い処理で失敗"を付与)
export const mapErr =
<E, F>(f: (error: E) => F) =>
<T>(r: Result<T, E>): Result<T, F> =>
isErr(r) ? err(f(r.error)) : r;
4-2. 例:エラーメッセージを丁寧にする💬🌸
type RawError = { message: string };
type FriendlyError = { userMessage: string };
const toFriendly = (e: RawError): FriendlyError => ({
userMessage: `ごめんね、うまくいかなかった🥺(${e.message})`,
});
const r1: Result<number, RawError> = err({ message: "在庫がありません" });
const r2 = mapErr(toFriendly)(r1);
// -> Err({ userMessage: "ごめんね、うまくいかなかった🥺(在庫がありません)" })
5. andThen:Resultを返す処理をつなぐ(最重要)⛓️🔥
5-1. map と andThen の違い(ここ超大事!)⚡
map:普通の値を返す変換(失敗しない前提の加工)andThen:Resultを返す変換(次の処理でも失敗しうる)
export const andThen =
<T, E, U>(f: (value: T) => Result<U, E>) =>
(r: Result<T, E>): Result<U, E> =>
isOk(r) ? f(r.value) : r;
5-2. “mapでResultを返す”とどうなる?😱(ネスト地獄)
const bad = map((n: number) => ok(n + 1))(ok(1));
// Result<Result<number, E>, E> みたいになりがち😵💫
5-3. andThenで解決!✨
const good = andThen((n: number) => ok(n + 1))(ok(1));
// Result<number, E> のまま🥰
6. match:最後に“読みやすく”取り出す🎀📦
if (isErr) return ... を最後に散らさないために、出口で match するのが気持ちいいよ🙂✨
export const match =
<T, E, R>(onOk: (value: T) => R, onErr: (error: E) => R) =>
(r: Result<T, E>): R =>
isOk(r) ? onOk(r.value) : onErr(r.error);
7. unwrapOr:失敗ならデフォ値で救う🛟🙂
「失敗したらとりあえず 0 で続けたい」みたいな時に使うよ(乱用は注意⚠️)
export const unwrapOr =
<T>(fallback: T) =>
<E>(r: Result<T, E>): T =>
isOk(r) ? r.value : fallback;
8. tryCatch:throw を Result に閉じ込める🧯🧼
現実はライブラリが throw することあるよね😭 だから「throwする関数」を Resultに変換できると便利!
export const tryCatch = <T, E>(
f: () => T,
onThrow: (cause: unknown) => E
): Result<T, E> => {
try {
return ok(f());
} catch (cause) {
return err(onThrow(cause));
}
};
9. 3段階処理を Result でつなぐ(ミニ演習)⛓️📝✨
題材:**「入力された予算(文字列)→ 数値化 → 上限チェック → 表示用フォーマット」**💖
9-1. エラー型(例)🏷️
type DomainError =
| { type: "InvalidNumber"; input: string }
| { type: "OverLimit"; limit: number; actual: number };
9-2. 3ステップ関数を作る🧩
import { Result, ok, err } from "./result";
const parseBudget = (input: string): Result<number, DomainError> => {
const n = Number(input);
return Number.isFinite(n)
? ok(n)
: err({ type: "InvalidNumber", input });
};
const ensureWithinLimit =
(limit: number) =>
(n: number): Result<number, DomainError> =>
n <= limit ? ok(n) : err({ type: "OverLimit", limit, actual: n });
const formatYen = (n: number): string => `${n.toLocaleString()}円`;
9-3. ヘルパーで“ネストなし”につなぐ💫
import { andThen, map, match } from "./result-helpers"; // さっき作ったやつ
const limit = 10000;
const result = andThen(ensureWithinLimit(limit))(
parseBudget("12000")
);
const message = match(
(n) => `OKだよ〜🙆♀️ 予算は ${formatYen(n)} だね✨`,
(e) => {
if (e.type === "InvalidNumber") return `数字で入れてね🥺(入力: ${e.input})`;
return `ごめんね、上限は ${formatYen(e.limit)} までだよ😭(入力: ${formatYen(e.actual)})`;
}
)(result);
console.log(message);
✅ ミニ演習(やってみてね)📝💖
- 入力を
"9999"に変えて、成功メッセージを確認😊 map(formatYen)を足して、表示用文字列への変換をパイプっぽくしてみる✨mapErrを使って、エラー文言の生成を 1か所に寄せる(おすすめ!)🎀
10. AI活用テンプレ🤖💞(レビュー役にして強くなる!)
🧑⚖️ 読みやすさレビュー
- 「この Result チェーン、読みやすくするリファクタ案を3つ出して。
mapとandThenの使い分けも指摘して」
🧪 テスト作成
- 「
map/mapErr/andThen/match/tryCatchの最小テストケースを vitest で作って。境界値も入れて」
🧹 エラーメッセージ整形
- 「DomainError を受け取って、ユーザー向け文言を統一トーンで返す関数を作って。優しい言い回し多めで」
11. 実務での選択肢:ライブラリを使うのも全然アリ🧰✨
学習で仕組みを理解したら、実務は neverthrow みたいな実績ある実装に寄せるのもアリだよ🙆♀️
Result と ResultAsync(Promise<Result>) を持ってて、map / mapErr / unwrapOr / andThen / match など揃ってる🪄 (GitHub)
(unwrap 事故を減らす eslint プラグイン推しもしてるよ)(GitHub)
12. まとめ:迷ったらこの早見でOK🧠✨
map:Okの値を変換(関数が Result を返さない)🍳andThen:次の処理が Result を返す時に使う(ネスト回避)⛓️mapErr:Errだけ変換(エラー統一・文脈付け)🧯match:出口で分岐して取り出す(UI/API変換の手前で便利)🎀tryCatch:throw を Result に閉じ込める🧯🧼
次の章(第19章)では、この考え方を Promise