【実務・中級編】引数に渡す「コールバック関数」の非同期エラーハンドリングを型で強制する – TypeScript コア・型システムの基礎解析バイブル

非同期コールバックの「隠れた地雷」を型で排除する:Safe Async Executionの設計指針

フロントエンド開発において、非同期処理のハンドリングは避けて通れない道だ。しかし、多くの現場で「なんとなく `try-catch` を書く」「なんとなく `Promise.catch` を繋ぐ」という属人的な運用に依存していないだろうか?

コンパイラの力を使い、「非同期エラーをハンドリングしていないコードは、そもそもビルドすら通さない」という強制力を持たせる。これがTypeScriptを「単なる型定義ツール」ではなく「堅牢なシステムを作るための静的解析エンジン」として使いこなすための第一歩だ。

今回は、コールバック関数に非同期処理を渡す際、呼び出し側に「エラーハンドリングの責任」を強いる高度な設計パターンを伝授する。

—

なぜ「ただの `(args: T) => Promise`」では不十分なのか

まず、アンチパターンを見てほしい。

// 悪い例:エラーハンドリングが呼び出し側に委ねられ、忘れ去られる
type Callback = (data: string) => Promise;

async function executeTask(cb: Callback) {
await cb(“payload”); // ここでrejectされたらどうなる?
}

この実装の問題点は明らかだ。呼び出し側が `executeTask(async () => { throw new Error(…) })` と書いたとき、そのエラーがどこで補足されるか、あるいは補足されずにイベントループを汚染するかが型からは一切読み取れない。

コンパイラに「エラーを返せ」と指示するには、戻り値の型を拡張し、成功と失敗を明確に分離する必要がある。

—

実践:Result型を用いた「強制エラーハンドリング」パターン

エラーを「例外」として投げるのではなく、「値」として型システムに取り込む。これには `Result` パターンを用いるのが最も堅牢だ。

// 成功と失敗を表現する共用体型
type Result =
| { success: true; value: T }
| { success: false; error: E };

// 非同期コールバックの定義
type SafeAsyncCallback = (arg: T) => Promise>;

/

  • コールバックの実行を保証するラッパー

/
async function runTask(arg: T, cb: SafeAsyncCallback): Promise {
const result = await cb(arg);

if (!result.success) {
// ここでエラーハンドリングのロジックを共通化できる
console.error(“Task failed:”, result.error);
throw result.error; // 必要に応じて再スロー
}

return result.value;
}

この設計の優位点

1. 静的解析の強化: 呼び出し側は `Promise>` を返すことを強要されるため、`try-catch` で囲むよりも、明示的なエラー分岐を書くことが自然な流れになる。
2. 型安全なエラー伝播: エラー型を汎用化することで、特定のドメインエラーを型として強制できる。

—

さらに一歩先へ:Promiseを強制的に「ハンドリング済み」にする型

コンパイル時に「`.catch()` を書いたか?」を完全に検知するのは難しいが、型定義で「エラーを返さなければならない」と縛ることは可能だ。

/

  • 呼び出し側にエラーハンドリングを強制する型
  • 戻り値を Result 型に限定することで、成功・失敗の判定を必須化する

/
type HandledPromise = Promise>;

function processData(
data: string,
onProcess: (d: string) => HandledPromise
) {
// onProcessを実行する際、呼び出し側は必ず success/error を判定しなければならない
return onProcess(data);
}

// 呼び出し側のコード
processData(“input”, async (d) => {
try {
return { success: true, value: d.length };
} catch (e) {
return { success: false, error: e as Error };
}
});

—

パフォーマンスと設計上の注意点

1. オーバーヘッドの最小化: `Result` オブジェクトの生成はメモリを消費するが、Node.jsや現代のブラウザエンジンでは、このような軽量なオブジェクトの割当は最適化の範囲内だ。むしろ、例外のスタックトレース生成コスト(非常に重い)を抑えられるため、頻発するエラーであれば `Result` パターンの方がパフォーマンスが高い。
2. Promise.allとの相性: `Result` パターンにすれば、`Promise.all` を使う際に「一部が失敗したときにどうするか」を型のレベルで制御できる。`.catch()` で飲み込むとすべてが成功扱いになってしまうが、`Result` なら `results.some(r => !r.success)` といった安全なチェックが可能だ。

チーフアーキテクトからの助言

TypeScriptの型システムは、単なるバリデーターではない。「ソフトウェアの設計図をコード化する言語」である。

「例外」はプログラムの流れを非局所的に乱す。もし君が堅牢なライブラリやコンポーネントを設計しているなら、例外を型で包み込み、呼び出し側に「この処理は失敗する可能性がある」と明示的に伝える設計を選択してほしい。

コードの行数は数行増えるかもしれない。だが、その数行が、深夜のデバッグで君自身を救うことになる。優れたアーキテクチャとは、「ミスをしてもシステムが壊れない場所」を型で定義することに他ならないのだから。

タイトルとURLをコピーしました