こんにちは!TypeScriptの世界へようこそ。フルスタックアーキテクトの先輩として、今日から君をさらに一段上のステージへ導く知見を授けましょう。
さて、今回はTypeScriptの関数における型定義、その中でも「非同期関数(Promise)のエラーハンドリング」という、実務で絶対に避けて通れない極めて重要なテーマを扱います。
他の言語(JavaやC#、あるいは素のJavaScript)からやってきた開発者が、一番最初に頭を悩ませるのが「非同期処理の中で起きたエラーをどう型安全に扱うか」という問題です。ここをクリアすれば、君の書くコードの堅牢性は劇的に跳ね上がりますよ。
—
1. なぜ「非同期エラーの型安全」が必要なのか?
まずは、普段私たちがやりがちな「危ういコード」から見ていきましょう。
TypeScriptで非同期関数を書くとき、こんな風に書いていませんか?
// 良くあるアンチパターン
async function fetchUserData(userId: string): Promise
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(“ユーザーの取得に失敗しました”); // ここで例外が投げられる
}
return response.json();
}
このコード、一見すると普通ですよね。でも、呼び出し元(Caller)の視点に立ってみると、大きな闇が隠されています。
try {
const user = await fetchUserData(“123”);
console.log(user.name);
} catch (error) {
// error の型は一体なに?
console.error(error);
}
TypeScriptの `catch (error)` の `error` は、デフォルトでは `unknown`型 になります。「何が飛んできたか分からないから、とにかく慎重に扱いなさい」というTypeScriptからの優しさ(あるいは厳しさ)です。
そのため、実務では `if (error instanceof Error)` といった型ガードを毎回書く必要があり、書き忘れると予期せぬバグの温床になります。「エラーが起きるかもしれないこと」が、関数の型定義を見ただけでは分からないのが最大の原因です。
—
2. 解決策:Result型(Either型)でエラーを値として扱う
世界最高峰のTypeScriptアーキテクトたちが好むモダンなアプローチは、「例外を投げる(throwする)のをやめ、エラーを戻り値(値)として型定義する」という手法です。
RustやGo言語に近いアプローチですが、TypeScriptの強力なユニオン型(Union Types)を使えば、美しくエレガントに実現できます。
以下の図解的なイメージを持ってください。
[非同期関数の実行]
│
├─ 成功した場合 ──> { success: true, data: T }
│
└─ 失敗した場合 ──> { success: false, error: E }
これをTypeScriptのコードに落とし込んでみましょう。
実装例:型安全なResult型パターン
// 1. 成功と失敗を表現するカスタム型の定義
type Result
| { success: true; data: T }
| { success: false; error: E };
// 2. 非同期関数にこのResult型を適用する
async function fetchUserDataSafe(userId: string): Promise
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
// 例外を投げず、「失敗した結果」を返す
return { success: false, error: “サーバーエラーが発生しました” };
}
const user: User = await response.json();
return { success: true, data: user };
} catch (err) {
// ネットワーク切断などの予期せぬ例外をキャッチしてラップする
return { success: false, error: “ネットワーク接続を確認してください” };
}
}
—
3. 呼び出し側で発揮される、圧倒的な型安全性
この設計の素晴らしいところは、呼び出し側のコードを書くときに、TypeScriptの補完(IntelliSense)が完璧に機能する点です。
async function handleLogin(userId: string) {
const result = await fetchUserDataSafe(userId);
// TypeScriptの「制御フロー分析」が働く!
if (!result.success) {
// このブロックの中では、result は { success: false; error: string } と確定する
console.error(result.error); // 文字列として安全に扱える!
return;
}
// このブロックの中では、result は { success: true; data: User } と確定する
console.log(`ようこそ、${result.data.name}さん!`); // User型として安全にアクセスできる!
}
`try/catch` を書く必要すらなくなり、関数の戻り値の型をチェックするだけで、エラーハンドリングが完全に強制されます。「エラー処理を書き忘れる」というヒューマンエラーを、コンパイラが完全に封じ込めてくれるわけです。
—
4. 陥りやすい文法エラーと注意点
ここで、初心者がよくハマりがちなポイントをいくつか解説しておきますね。
① `async` 関数は自動的に `Promise` でラップされる
「あれ? `Result
// ❌ 間違い
async function badFunc(): Result
return { success: true, data: … }; // エラー! Promise
}
`async` キーワードがついた関数は、必ずその戻り値を `Promise` で包み込みます。そのため、関数の型定義は必ず `Promise
// ⭕ 正解
async function goodFunc(): Promise
return { success: true, data: … };
}
② `any` や `unknown` で逃げない
`catch (e)` の中で面倒くさくなって `(e as any).message` のように型アサーション(強制型変換)で逃げたくなる気持ちは分かります。しかし、それではTypeScriptを使っている意味が薄れてしまいます。
上記で紹介したように、あらかじめエラーの型(`E`)をジェネリクスで定義し、ラップして返す癖をつけましょう。
—
まとめ:ここをクリアすれば、もう怖くない!
今回は、戻り値として `Promise
- 従来の `throw` と `try/catch` は、エラーの型が `unknown` になりがちで、ハンドリングが漏れやすい。
- `Result
` 型 を用いて成功と失敗を値として表現することで、関数の戻り値からエラー処理を強制できる。 - `async` 関数は `Promise` を返すため、戻り値の型は `Promise
>` にする。
このパターンをマスターすると、君が書く非同期コードの信頼性はプロのエンジニア水準に達します。ぜひ、次からの開発で試してみてくださいね。
ここをクリアできれば、TypeScriptの基本はバッチリマスターできたも同然です!次のステップへ進みましょう!