はじめに
前回
JavaScript の非同期処理は try/catch と Promise.reject が混在しやすく、
エラー経路が複雑になりがちである。TypeScript では Async/Result 型パターン を採用することで、
成功・失敗を静的に区別しつつフロー制御を簡潔に保てる。本稿では以下を解説する。
- Result 型の定義とユーティリティ
-
async/awaitとの統合 - 実務 API ラッパーへの組み込み
- 並列処理での合成パターン
- 落とし穴とパフォーマンス考慮
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) を取り上げ、読み取り/書き込みを型で完全に分離するアーキテクチャを解説する。