こんにちは!フロントエンドから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
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
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の型システムは、あなたのコードの「お守り」であり、最高の設計図です。ぜひ明日の開発から取り入れてみてくださいね。それでは、次のステップへ進みましょう!