こんにちは!TypeScriptの世界へようこそ。フロントエンドからバックエンドまで、型安全で美しいアーキテクチャを作る楽しさを一緒に味わっていきましょう。
今回は、TypeScriptの型システムをマスターする上で最もエキサイティングで実用的なテーマ、「Type Alias(型エイリアス)で実現する代数的データ型(ADT)のモデリング手法」についてお話しします。
「代数的データ型?なんだか難しそうな名前だな…」と思いましたか?
大丈夫です。要するに、「起こりうる状態やデータをパズルのピースのように組み合わせ、不正な状態を型レベルで絶対に作れなくする技術」のことです。ここをクリアすれば、あなたの書くコードの安全性と美しさは劇的に跳ね上がりますよ。
それでは、一緒に深掘りしていきましょう!
—
1. なぜ「代数的データ型(ADT)」が必要なのか?
実務でコードを書いていると、次のようなフラグだらけの状態管理に出会ったことはありませんか?
// ❌ ありがちだけど危険なアンチパターン
interface UserState {
isLoading: boolean;
data: User | null;
error: Error | null;
}
このインターフェース、一見普通に見えますが、TypeScriptの型システムの観点からは「穴だらけ」です。
考えてみてください。`isLoading` が `true` なのに、同時に `data` も `error` も存在する……なんて矛盾した状態も、この型では表現できてしまいますよね。これがバグの温床になります。
ここで登場するのが、TypeScriptの Union Types(直和型) を使った代数的データ型です。
—
2. Type Alias × Union Types で状態をモデリングする
代数的データ型(Algebraic Data Types)の基本は、「A または B または C」という排他的な状態(直和型)を表現することです。
ユーザー情報を取得する非同期処理を例に、綺麗なモデリングを見てみましょう。
// — 1. 各状態をオブジェクトの型として定義する —
// ① 待機(アイドル)状態
type IdleState = {
status: “IDLE”;
};
// ② ローディング(読込中)状態
type LoadingState = {
status: “LOADING”;
};
// ③ 成功状態(データを持つ)
type SuccessState
status: “SUCCESS”;
data: T; // ジェネリクスを使ってどんなデータも受け取れるように
};
// ④ 失敗状態(エラーを持つ)
type FailureState = {
status: “FAILURE”;
error: Error;
};
// — 2. これらをUnion(直和)で結合する(これが代数的データ型!) —
type RequestState
このコードの何がスゴいのか?
ここで `status` プロパティに注目してください。`”IDLE”`, `”LOADING”` といった具体的な文字列型(リテラル型)をあえて入れています。
この共通のキー(専門用語で タグ付きUnion / 構文タグ と呼びます)があるおかげで、TypeScriptのコンパイラは「あ、今どの状態にいるのか」を完璧に識別できるようになるのです。
—
3. 網羅性チェック(Exhaustiveness Checking)の魔法
状態をモデリングできたら、次はそれを画面やロジックでどう扱うか、ですね。
ここでTypeScriptの真骨頂である「型ガード(Narrowing)」が活きてきます。
interface User {
id: string;
name: string;
}
// 状態に応じたメッセージを返す関数
function handleUserUI(state: RequestState
switch (state.status) {
case “IDLE”:
return “ボタンを押してデータを取得してください。”;
case “LOADING”:
return “読み込み中……少々お待ちください。”;
case “SUCCESS”:
// このブロック内では、stateは自動的に SuccessState
return `ようこそ、${state.data.name}さん!`;
case “FAILURE”:
// このブロック内では、stateは FailureState に絞り込まれる!
return `エラーが発生しました: ${state.error.message}`;
}
}
ここで知っておくべき「コンパイラの知見」
「もし将来、新しい状態(例えば `Refresing`)が `RequestState` に追加されたらどうなるの?」と思いますよね。
TypeScriptなら、コンパイラが漏れを教えてくれる仕組み(網羅性チェック)を作ることができます。
お馴染みの `never` 型を使ったテクニックを見てみましょう。
function assertNever(x: never): never {
object: x; // ここに到達してはならない
throw new Error(`予期しない状態です: ${JSON.stringify(x)}`);
}
function handleUserUISafe(state: RequestState
switch (state.status) {
case “IDLE”: return “IDLE”;
case “LOADING”: return “LOADING”;
case “SUCCESS”: return “SUCCESS”;
case “FAILURE”: return “FAILURE”;
default:
// ✨ もし上のcaseに漏れがあれば、ここでの state は never ではなくなり、コンパイルエラーになる!
return assertNever(state);
}
}
この `assertNever` パターンを仕込んでおけば、チーム開発で新しい状態を追加したときに、「状態のハンドリングを書き忘れている箇所」をコンパイルエラーで100%検出できます。これぞプロのアーキテクチャです!
—
4. 陥りがちな文法エラーと注意点
初心者の頃や、他の言語(OOP言語など)から移ってきたときによやりがちなミスをいくつか挙げておきますね。
① interface と type alias の混同
「どっちを使えばいいの?」と迷うかもしれませんが、今回の代数的データ型(Union型)を作る場合は、Type Alias (`type`) が必須です。`interface` 同士のUnionも作れなくはないですが、拡張性やリテラルの組み合わせにおいては `type` の方が圧倒的に直感的で強力です。
② タグ(共通プロパティ)の付け忘れ
以下のようなUnionを作ってしまうと、TypeScriptは区別がつかなくなります。
// ❌ どの型か判別できない(構造的型付けの罠)
type BadState = { data: string } | { error: Error };
これだと、TypeScriptは「データがあるのかエラーなのか」を自動判別できません。必ず `status: “SUCCESS”` や `type: “success”` のような識別子(タグ)を各オブジェクトに持たせるようにしてくださいね。
—
まとめ
いかがだったでしょうか?
- Type Alias と Union Types を組み合わせることで、複雑な状態を安全にモデリングできる。
- タグ付きUnion により、実行時エラーになりうる「不正な状態」を型レベルで排除できる。
- `switch` 文と `never` 型による網羅性チェックで、改修に強い堅牢なコードが書ける。
ここをクリアできれば、あなたのTypeScriptのスキルはもう基礎の域を超え、実務で即戦力となるアーキテクチャ設計の領域に到達しています。
ぜひ、明日のコードから「フラグの乱立」をやめて、代数的データ型による美しい状態管理を取り入れてみてください。バグの少ない、手に取るようにスムーズな開発体験が待っていますよ!