【入門編】引数に渡す「コールバック関数」の型定義における、非同期処理の例外伝播の型安全化 – TypeScript コア・型システムの基礎解析バイブル

こんにちは!フロントエンドからNode.jsまで、TypeScriptのことなら何でも聞いてくださいね。

TypeScriptを書き始めると、「関数を引数に取る関数(高階関数)」や「非同期処理(Promise)」にたくさん出会うようになりますよね。特に、「他の人に使ってもらう関数」や「共通の処理を行うラッパー関数」を作るとき、引数に渡されるコールバック関数が非同期(`Promise`)だったらどう型定義すればいいのか?、そしてそこで発生したエラーを取りこぼさないようにするにはどうすればいいのか?という壁にぶつかりがちです。

ここをクリアすれば、あなたの書くTypeScriptコードの安全性は一段と跳ね上がりますよ。今日は、非同期コールバックの型制約と例外伝播の型安全化について、本質から優しく紐解いていきましょう!

—

1. なぜ「非同期のコールバック」で型安全が必要なのか?

まずは、私たちがよくやりがちな「危ういコード」から見てみましょう。
例えば、何らかの処理の「前後にログを仕込むラッパー関数」を書きたいとします。

// ❌ やりがちな危ういコード
function withLogging(callback: () => void) {
console.log(“処理開始…”);
callback(); // ← ここで非同期関数が渡されたらどうなる?
console.log(“処理終了!”);
}

この `withLogging` 関数に、APIリクエストのような非同期処理(`async/await`)を渡したくなりました。

withLogging(async () => {
// 意図的にエラーを投げる非同期処理
throw new Error(“データベース接続エラー!”);
});

このコードを実行すると、コンソールにはどう表示されるでしょうか?

処理開始…
処理終了!
(少し遅れて)Uncaught (in promise) Error: Database connection error!

おや……? 「処理終了!」がエラーが発生するよりも先に出力されてしまっていますよね。
これは、`callback` が返す `Promise` の完了を `withLogging` が全く待っていない(`await` していない)からです。さらに最悪なことに、コールバック内で起きたエラーが呼び出し元に伝播せず、ハンドリング漏れ(Uncaught Promise Rejection)を引き起こしています。

初学者のうちは「動いているように見えてバグの温床になる」という、TypeScriptが最も防ぎたい罠がここに潜んでいるのです。

—

2. 解決策:戻り値を `Promise` に制約し、しっかりと `await` する

この問題を解決するには、TypeScriptの型システムを使って「コールバック関数は必ず Promise(非同期)を返しなさい」「そしてラッパー側でそれを待ち合わせなさい」と強制すればよいのです。

実際のコードを見てみましょう。

/

  • ✅ 堅牢な非同期コールバックの型定義

/
async function withLoggingSafe(callback: () => Promise): Promise {
console.log(“【ログ】処理を開始します…”);

try {
// 1. コールバックが返すPromiseの完了を確実に待つ(例外もここでキャッチ可能に)
await callback();
console.log(“【ログ】処理が正常に完了しました。”);
} catch (error) {
console.error(“【ログ】エラーを検知しました!”, error);

// 2. エラーを握りつぶさずに、呼び出し元へ安全に再スロー(伝播)する
throw error;
}
}

コードの意味を紐解く

1. `callback: () => Promise`
引数に取るコールバック関数は、「引数なしで、戻り値として `Promise`(非同期処理)を返すもの」に限定しています。これにより、うっかり同期関数が渡されたり、戻り値のない関数が渡されたりするミスをコンパイル時に防げます。
2. `await callback();`
渡された非同期処理の完了(または失敗)をしっかりと待機します。
3. `try / catch` と `throw error`
非同期処理の中で起きた例外を確実にキャッチしログに残しつつ、呼び出し側でもエラーハンドリングができるように再スロー(Propagate)しています。

—

3. さらに実践的! 戻り値の型をジェネリクス(総称型)で柔軟にする

先ほどの例では `Promise` に固定していましたが、実際の開発では「非同期コールバックの実行結果(データ)を受け取りたい」場面のほうが多いはずです。

ジェネリクス(``)を使って、どんな型のデータでも受け取れる汎用的なラッパーに進化させてみましょう。

/

  • 🚀 任意の戻り値型に対応するプロフェッショナルな非同期ラッパー

/
async function executeSafely(callback: () => Promise): Promise {
try {
// コールバックの結果(T型のデータ)をそのまま受け取る
const result = await callback();
return result;
} catch (error) {
// 共通のエラーハンドリング処理(Sentryへの送信やログ出力など)
console.error(“[System Error Captured]:”, error);

// 呼び出し元が catch できるように例外をそのまま流す
throw error;
}
}

呼び出し側のコード

async function fetchUserProfile(userId: string) {
// executeSafely を使うことで、型安全とエラー伝播が完全に保証される
const user = await executeSafely(async () => {
const response = ;
if (!response.ok) {
throw new Error(“ユーザー情報の取得に失敗しました”);
}
return response.json(); // ここで推論された型がそのまま外側に伝播する
});

console.log(user.name); // user の型もちゃんと推論されます!
}

—

4. 陥りやすい文法エラーと注意点

ここで、よくある間違いについても触れておきますね。

罠:`async` 関数は何も返さなくても `Promise` を返すから大丈夫……ではない!

JavaScript/TypeScriptの仕様上、`async` がついた関数は自動的に `Promise` を返します。しかし、型定義が `() => void` や `() => any` になっていると、TypeScriptは「非同期エラーのハンドリング漏れ」に気づくことができません。

  • `() => void`

→ 「戻り値は何でもいい(無視する)」という意味になるため、同期関数を渡してもエラーになりません。非同期の例外を待ち受けることができなくなります。

  • `() => Promise`

→ 「必ず非同期処理を返しなさい」という強い制約になります。

コールバックが非同期であることが前提の処理を書くときは、必ず戻り値の型に `Promise<...>` を明記するように心がけましょう。

—

まとめ

いかがでしたか? 今回のポイントをギュッと凝縮して振り返ってみましょう。

  • 非同期コールバックの型は `() => Promise` のように明示的に Promise を返すよう制約する。
  • ラッパー側で `await callback()` を使うことで、非同期処理の完了待ちと例外のキャッチを確実に行う。
  • キャッチしたエラーはそのまま `throw error` することで、呼び出し側への例外伝播(エラーハンドリングの権利)を担保する。

ここをマスターすれば、非同期処理が絡む複雑なカスタムフックや、SDK、共通ユーティリティ関数を書くときも、恐れることなくエレガントな型定義ができるようになります。

TypeScriptの型システムは、あなたのコードの「お守り」であり、最高の設計図です。ぜひ明日の開発から取り入れてみてくださいね。それでは、次のステップへ進みましょう!

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