こんにちは!TypeScriptの型システムの世界へようこそ。
フロントエンドからバックエンドまで、型安全なコードを書く楽しさに魅せられて日々コードを書いています。
他のオブジェクト指向言語(JavaやC#など)からTypeScriptに入ってきた開発者の方から、よくこんな質問を受けます。
「インターフェースと型エイリアス、どっちを使えばいいの? 複雑な状態を安全に表現するにはどうしたらいいの?」と。
結論からお伝えすると、型エイリアス(`type`)と「判別可能な共用体(Discriminated Unions)」を組み合わせることで、「不正な状態を型レベルで絶対に存在させない」という、TypeScriptならではの最強の武器を手に入れることができます。
ここをクリアすれば、あなたのTypeScriptのスキルは確実に次のステージに進みます。一緒に優しく、深く、マスターしていきましょう!
—
1. なぜ「普通のオブジェクト型」ではバグを防げないのか?
例えば、Webアプリでよくある「API通信の状態(ローディング中・成功・エラー)」を管理する場面を想像してください。
初学者の頃は、ついこんなふうに1つの大きな型を作りがちです。
// 良くない例:すべてのプロパティをオプショナル(省略可能)にしてしまう
type BadState = {
isLoading: boolean;
data?: string;
error?: Error;
};
この型には、大きな罠(バグの温床)があります。TypeScriptのコンパイラは、こんな「矛盾した状態」をも許可してしまいます。
- `isLoading: false` なのに、`data` も `error` も無い。
- `isLoading: true` なのに、ちゃっかり `data` が入っている。
「ローディング中なのにデータが存在する」なんて、実行時にはバグの元ですよね。でも、この `BadState` 型だと、TypeScriptは何も怒ってくれません。
ここで登場するのが、判別可能な共用体(Discriminated Unions)です。
—
2. 判別可能な共用体(Discriminated Unions)とは?
考え方はとてもシンプルです。それぞれの状態を表す型に、「共通の目印(タグ)」を持たせるのです。
イメージ図:
[ State 型 ]
├── LoadingState (tag: “LOADING”)
├── SuccessState (tag: “SUCCESS”, data: string)
└── ErrorState (tag: “ERROR”, error: Error)
これをTypeScriptの型エイリアスで表現してみましょう。
// 1. ローディング中の状態
type LoadingState = {
tag: “LOADING”; // ←これが「タグ(判別子)」です
};
// 2. 成功時の状態
type SuccessState = {
tag: “SUCCESS”; // ←お揃いの名前のプロパティ
data: string; // 成功時だけに存在するデータ
};
// 3. エラー時の状態
type ErrorState = {
tag: “ERROR”; // ←お揃いの名前のプロパティ
error: Error; // エラー時だけに存在するオブジェクト
};
// これらをパイプ(|)で繋いだものが「判別可能な共用体」です
type AppState = LoadingState | SuccessState | ErrorState;
この `AppState` は、「ローディング中 または 成功 または エラー」のどれか一つだけに状態が限定されます。「ローディング中なのにデータがある」という矛盾した不正なデータ構造を、型レベルで物理的に作れなくなるのです。これが圧倒的な強さの秘密です。
—
3. `switch` 文と「網羅性チェック(Exhaustiveness Checking)」
さて、この `AppState` を実際にコンポーネントや関数で処理するとき、どうやって中身を取り出せばいいでしょうか?
ここで `switch` 文(または `if` 文)を使います。TypeScriptの賢いところは、タグ(`tag`)の値をチェックするだけで、自動的にその状態の型へ絞り込んでくれる(Narrowing)点です。
function handleApp(state: AppState) {
switch (state.tag) {
case “LOADING”:
// ここに入った瞬間、TypeScriptは state が LoadingState だと確信します
console.log(“読み込み中…”);
break;
case “SUCCESS”:
// ここでは SuccessState に絞り込まれるので、data を安全に触れる!
console.log(`成功: ${state.data.toUpperCase()}`);
break;
case “ERROR”:
// ここでは ErrorState に絞り込まれる
console.error(`エラー発生: ${state.error.message}`);
break;
}
}
🧠 さらに一歩進む:網羅性チェック(Exhaustiveness Checking)の魔法
現場で本当によくあるのが、「将来、新しい状態(例: `RETRYING`)を追加したのに、処理するコード側で `case` を書き忘れてバグる」というミスです。
これをTypeScriptのコンパイラに検知させるテクニックが、網羅性チェックです。
関数の一番最後に、以下のようなコードを仕込んでみてください。
// 意図しない値が入ってきたときに、型エラーを起こすためのヘルパー関数
function assertNever(x: never): never {
ofthrow new Error(`予期せぬ状態です: ${JSON.stringify(x)}`);
}
function handleAppStrict(state: AppState) {
switch (state.tag) {
case “LOADING”:
return “Loading…”;
case “SUCCESS”:
return state.data;
case “ERROR”:
return state.error.message;
default:
// 【重要】もし上のcaseで網羅しきれていない状態があれば、
// ここに到達した時の state の型は「never(何もありえない)」ではなくなってしまうため、
// コンパイルエラーになります!
return assertNever(state);
}
}
もしあなたが未来の開発で、`AppState` に新しい状態 `TimeoutState` を追加したとします。その瞬間、上の `handleAppStrict` 関数は「おい、`TIMEOUT` の処理ケースが書き忘れてるぞ!」とコンパイルエラーを吐いて教えてくれます。
人間はうっかり忘れる生き物ですが、TypeScriptのコンパイラは決して忘れません。この仕組みを仕込んでおけば、大規模なリファクタリングも怖くなくなりますよね。
—
4. まとめ
ここまでのポイントを綺麗にまとめておきますね。
1. 型エイリアス(`type`)を使って、状態ごとのオブジェクト型を定義する。
2. それぞれの型に共通のプロパティ(例: `tag: “…”`)を持たせて「判別可能な共用体」にする。
3. `switch` 文でタグを判定すると、TypeScriptが自動的に型を安全に絞り込んでくれる(Narrowing)。
4. `default` 節で `never` 型を使った網羅性チェックを行い、将来のうっかりミス(書き忘れ)をコンパイル時に完全に防ぐ。
このパターンをマスターすると、ReduxやZustandなどの状態管理、複雑なフォームのバリデーション、APIレスポンスのハンドリングなど、あらゆる場面で「堅牢で美しいコード」が書けるようになります。
「ここをクリアすれば、TypeScriptの基本はバッチリマスターできますよ!」
ぜひ、今日書くコードのどこかに「判別可能な共用体」を取り入れてみてくださいね。あなたのTypeScriptライフがより一層楽しいものになりますように!