2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TSの鬼 第17回:Async/Result 型パターンでエラーハンドリングを統一せよ

2
Posted at

はじめに

前回

JavaScript の非同期処理は try/catchPromise.reject が混在しやすく、
エラー経路が複雑になりがちである。TypeScript では Async/Result 型パターン を採用することで、
成功・失敗を静的に区別しつつフロー制御を簡潔に保てる。本稿では以下を解説する。

  1. Result 型の定義とユーティリティ
  2. async/await との統合
  3. 実務 API ラッパーへの組み込み
  4. 並列処理での合成パターン
  5. 落とし穴とパフォーマンス考慮

1. Result 型を定義する

export type Ok<T> = { ok: true; value: T };
export type Err<E = unknown> = { ok: false; error: E };
export type Result<T, E = unknown> = Ok<T> | Err<E>;
  • 成功・失敗を ok フラグ で区別し、条件分岐で型が自動絞り込み。

1.1 ヘルパー関数

export const ok = <T>(value: T): Ok<T> => ({ ok: true, value });
export const err = <E>(error: E): Err<E> => ({ ok: false, error });

2. async 関数で Result を返す

async function fetchJson<T>(url: string): Promise<Result<T, Error>> {
  try {
    const res = await fetch(url);
    if (!res.ok) return err(new Error(res.statusText));
    return ok(await res.json());
  } catch (e) {
    return err(e as Error);
  }
}
  • 例外を内部で吸収 し、呼び出し側は try/catch 不要。
  • 戻り値は Result<T,Error> で型安全に分岐可能。

2.1 使用例

const res = await fetchJson<User>("/api/user/1");
if (res.ok) {
  console.log(res.value.name);
} else {
  console.error(res.error.message);
}

3. React Query と統合する

const useUser = (id: number) =>
  useQuery(["user", id], () => fetchJson<User>(`/api/user/${id}`), {
    select: (r) => (r.ok ? r.value : undefined),
    onError: (r) => console.error((r as Err).error),
  });
  • select で成功値のみ抽出し、コンポーネントは User | undefined を扱う。

4. Result を合成するユーティリティ

4.1 andThen(flatMap)

export function andThen<T, U, E>(
  r: Result<T, E>,
  f: (v: T) => Result<U, E>
): Result<U, E> {
  return r.ok ? f(r.value) : r;
}

4.2 all(タプル版)

export function all<T extends readonly Result<any, any>[]>
  (results: T): Result<{ [K in keyof T]: T[K] extends Ok<infer U> ? U : never }, T[number] extends Err<infer E> ? E : never> {
  const values: any[] = [];
  for (const r of results) {
    if (!r.ok) return r;
    values.push(r.value);
  }
  return ok(values as any);
}
  • 並列結果の 最初のErrを短絡 し、成功タプルを返す。

5. パターン応用:フォームバリデーション

function validateEmail(s: string): Result<string, string> {
  return /.+@.+\..+/.test(s) ? ok(s) : err("invalid email");
}
function validateAge(n: number): Result<number, string> {
  return n >= 18 ? ok(n) : err("age must be 18+");
}

const formResult = all([
  validateEmail(input.email),
  validateAge(input.age),
]);
  • all でエラーを早期返却し、成功時は const [email, age] = formResult.value と安全に分割。

6. 落とし穴と対策

落とし穴 原因 対策
Result ネスト地獄 andThen 連鎖不足 pipe() ユーティリティで関数合成
エラー型が unknown で曖昧 catch で暗黙 any z.ZodError など具体型にマッピング
ランタイム例外の漏れ 非同期コード内で throw すべて Result にラップし void 関数にも注意

まとめ

  • Result 型で 成功と失敗を値レベルで分離 し、try/catch を排除。
  • ジェネリクスとユーティリティ (andThen, all) で 合成可能なフロー を構築。
  • React Query やフォーム検証など、実務ユースケースに直結。

次回は 型安全 CQRS(Command Query Responsibility Segregation) を取り上げ、読み取り/書き込みを型で完全に分離するアーキテクチャを解説する。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?