こんにちは!TypeScriptの型システムの世界へようこそ。
他のオブジェクト指向言語(JavaやC#、あるいは動的言語のJavaScriptなど)からTypeScriptに入ってきたとき、最初に「おっ?」と立ち止まるポイントが、この「トップ型」と「ボトム型」の概念なんですよね。
`unknown`、`any`、そして `never`。
これらの型は、TypeScriptの型システム全体をピラミッドのように支える、いわば「超重要人物」たちです。ここをしっかりクリアすれば、TypeScriptの型安全の仕組みはバッチリマスターできますよ。
今日は、インターフェースや型エイリアスと組み合わせながら、現場で即座に役立つ「堅牢なエラーハンドリング」を実装する方法を、一緒に紐解いていきましょう!
—
1. 型のピラミッド構造をイメージしよう
TypeScriptの型システムは、すべての型を網羅する巨大な「型の階層構造(包含関係)」を持っています。
まずは、頭の中で次のようなピラミッドをイメージしてみてください。
[ トップ型 ] ← すべてを包み込む神の領域 (unknown, any)
↓
(通常の型たち) ← string, number, Userインターフェースなど
↓
[ ボトム型 ] ← 何も存在しない虚無の領域 (never)
このピラミッドの頂点に君臨するのが「トップ型」、そして最底辺に位置するのが「ボトム型」です。それぞれの特徴を優しく見ていきましょう。
—
2. トップ型:すべてを受け入れる懐の深さ(`unknown` と `any`)
トップ型とは、「あらゆる型の子孫(代入可能な値)」を意味します。つまり、どんな値であっても、トップ型の変数に格納することができます。
ここに登場するのが、おなじみの `any` と、少し厳格な親戚である `unknown` です。
ざっくり言うと:
- `any`: 「型チェックを放棄する禁断の呪文」。何でも許しますが、TypeScriptの恩恵(補完や安全性)がすべて消え去ります。
- `unknown`: 「安全な何者か分からないもの」。何でも受け入れますが、使うときは必ず型を絞り込まないと怒られます。
インターフェースや型エイリアスでの実践例
例えば、外部のAPIから帰ってきたレスポンスデータを扱う型エイリアスを考えてみましょう。
type ApiResponse = {
status: number;
// レスポンスのデータ構造が状況によって変わるため、ここでは一旦トップ型にする
data: unknown;
};
const response: ApiResponse = {
status: 200,
payload: { userId: “123”, name: “Alice” } // どんなデータも入る
};
ここで `response.data` をそのまま使おうとすると、TypeScriptのコンパイラはこう言います。
> 「おいおい、`unknown`型の中身が何だか分からないのに、勝手にプロパティにアクセスしちゃダメだよ!」
これが `unknown` の優しさです。これを使うには、「型ガード(Narrowing)」という手順を踏む必要があります。
function handleResponse(res: ApiResponse) {
// 安全確認(型チェック)を行う
if (typeof res.data === “object” && res.data !== null && “name” in res.data) {
// このブロックの中に入った瞬間、TypeScriptは `res.data` が何者かを理解する!
console.log((res.data as { name: string }).name);
} else {
console.error(“予期せぬデータ構造です”);
}
}
実務では、バグの温床になる `any` は極力封印し、安全に型を検証できる `unknown` を使っていくのがモダンなプロフェッショナルの作法です。
—
3. ボトム型:何も存在しない虚無の領域(`never`)
逆に、ピラミッドの最底辺に位置するのがボトム型である `never` です。
`never` は、「決して起こり得ない値の型」を意味します。
「何も値が入らないなら、何の役に立つの?」と思いますよね。実はこの `never` こそが、堅牢なエラーハンドリングや網羅性チェックにおいて「最強の武器」になるんです。
陥りがちな誤解:`void` との違い
- `void` は、「何も返さない関数が返す型」(undefinedに近い)。
- `never` は、「そもそも正常に終了しない(あるいは絶対に値が存在しない)」ことを表す型。
—
4. 実践:`unknown` と `never` で作る最強のエラーハンドリング
それでは、ここまでの知識を総動員して、実務でそのまま使える「堅牢なエラーハンドリング」を書いてみましょう。
よくある、APIリクエストで失敗したときのエラーをキャッチするコードです。
// 1. エラーメッセージを表現する型エイリアス
type AppError = {
code: string;
message: string;
};
// 2. 予期せぬエラーを安全に処理する関数
function handleError(error: unknown): void {
// unknown型なので、まずErrorオブジェクトかどうかをガードする
if (error instanceof Error) {
console.error(`通常のエラー: ${error.message}`);
}
// 自作のAppError構造体かチェック
else if (isAppError(error)) {
console.error(`アプリ固有エラー [${error.code}]: ${error.message}`);
}
// それ以外の完全に正体不明の何か(文字列や数値がthrowされた場合など)
else {
// ここでボトム型 `never` が真価を発揮する!
// もし全てのパターンを網羅していれば、この変数に入る値は「存在しない(never)」はず
const exhaustiveCheck: never = error;
console.error(“未知の致命的エラー:”, exhaustiveCheck);
}
}
// 型ガード用のヘルパー関数
function isAppError(arg: unknown): arg is AppError {
return (
typeof arg === “object” &&
arg !== null &&
“code” in arg &&
typeof (arg as AppError).code === “string” &&
“message” in arg &&
typeof (arg as AppError).message === “string”
);
}
ここがプロの技!網羅性チェック(Exhaustive Check)
例えば、次のような「処理の状態(ステータス)」を表す型があったとします。
type Status = “loading” | “success” | “error”;
function handleStatus(status: Status) {
switch (status) {
case “loading”:
return “読み込み中…”;
case “success”:
return “成功!”;
case “error”:
return “失敗…”;
default:
// 【重要】もし将来、Status型に “timeout” が追加されたのに、
// このswitch文に書き忘れた場合、TypeScriptはここでコンパイルエラーを出してくれます!
const _exhaustiveCheck: never = status;
return _exhaustiveCheck;
}
}
もし `status` に `”timeout”` が追加され、`case “timeout”:` の書き忘れが発生した場合、`default` ブロック内の `status` はもはや `never` ではなく `”timeout”`型になってしまいます。
「`timeout` という値を、`never` 型の変数には入れられません!」というコンパイルエラーが起きるため、バグが本番環境に出る前に、コードを書いている段階で気づくことができるのです。これが `never` 型の最高の活用法です。
—
まとめ
- トップ型 (`unknown`): 「何が来るか分からない」を安全に扱うための入り口。使うときは必ず型ガードを通す。
- ボトム型 (`never`): 「絶対にあり得ない」を表現し、コンパイラに網羅性を強制させるための切り札。
この2つをインターフェースや型エイリアスと組み合わせることで、TypeScriptの型システムは単なる「文字の補完ツール」から、あなたのコードベースを守り抜く「強固な防壁」へと進化します。
ここをクリアできれば、TypeScriptの基本はバッチリマスターできていますよ!自信を持って、次のステップへ進んでくださいね。それではまた!