タグ付き共用体(Tagged Union)は、ひとつの値が複数の「形」のうちどれか1つを確実に表すためのデータ構造です。
各「形」には判別用のタグが付き、どの形かを安全に見分けられます。
これにより条件分岐のミスを防ぎ、読みやすいコードを書きやすくなります。
初心者の方でも、使い方のパターンを覚えればすぐに活用できます。
タグ付き共用体(Tagged Union)とは
初心者向けの基本
用語の整理
タグ付き共用体は「値の種類ごとに名前(タグ)が付いた入れ物」で、取りうる形を明示的に列挙する考え方です。
それぞれの形をバリアント(variant)と呼び、例えば「現金」か「カード」のように互いに排他的な状態を1つだけ持ちます。
簡単なイメージ
例えば支払い方法を表すとき、ただの文字列だと”cash”や”card”の綴りを間違えるかもしれません。
タグ付き共用体ならCashとCardという決まった選択肢だけを許し、さらにCardには「下4桁」などの付随データも一緒に持てます。
「どの種類の値か」と「その値に必要な追加情報」を1セットで扱えることがポイントです。
合併型(Union)との違い
違いの概要
合併型(Union)は「AかBかC…のどれか」という幅の広い型を指します。
タグ付き共用体はその一種ですが、各ケースに必ず「タグ(判別子)」が付いている点が特徴です。
これにより、分岐時に「今はどのケースか」を安全かつ簡単に判定でき、取りこぼしやタイプミスを防げます。
以下は違いの整理です。
| 項目 | 単純な合併型(Union) | タグ付き共用体(Tagged Union) |
|---|---|---|
| 自己説明性 | どの形か分かりにくい | タグで一目で分かる |
| 実行時の安全性 | 誤った型扱いが起きやすい | 不正な扱いを型で防げる |
| コンパイル時チェック | 限定的 | 網羅性チェックが効きやすい |
| 分岐の書きやすさ | 条件が複雑になりやすい | タグでシンプルに分岐 |
| IDEの補完 | ばらつきがある | ケースごとの補完が強力 |
結論として、タグ付き共用体は「安全に分岐できる合併型」です。
使える言語例
- Rust(enum): タグ付き共用体を言語機能として提供します。
- TypeScript(判別可能なユニオン): オブジェクトのプロパティをタグにして使います。
- Swift(enum with associated values): 値付きケースを自然に表現できます。
- Kotlin(sealed class/enum class): 継承と組み合わせて安全な分岐が可能です。
- F#/OCaml/Haskell(代数的データ型): もともとこの考え方が基本にあります。
- C/C++: unionと手動タグ(構造体のフィールド)を組み合わせて実現します。
タグ付き共用体のメリット
型安全でミスを防ぐ
存在しない値や不完全な状態を、コンパイル時に防げます。
例えば「カード」なのにカード情報が空、という状態を型が許さないように設計できます。
結果として、実行時のバグを減らしやすくなります。
パターンマッチで読みやすい
タグがあるので、ケースごとに分岐するコード(パターンマッチ)が簡潔で読みやすくなります。
分岐条件が明確になるため、レビューや保守のコストも下がります。
nullを避ける設計
nullを使わずに「ある/ない」を表現するOption型のようなパターンが自然に書けます。
「ない」も1つのバリアントとして明示することで、見落としを防げます。
エラー処理に強い
成功と失敗を1つの型で表すResult型のパターンにより、例外に頼らず安全にエラーを扱えます。
戻り値の型だけで「成功か失敗か」と「必要な情報」が分かります。
データ構造をシンプルに保つ
複数のフラグやnullチェックが散らばる設計を避け、「今はこの状態」という1点に絞ったデータ構造にできます。
状態遷移も追いやすくなり、ロジックが見通しやすくなります。
プログラミングでの使い方
バリアントを定義する
Rustならenum
各バリアントを列挙して定義します。
値を持つバリアントも簡単です。
TypeScriptなら判別子プロパティ
オブジェクトにkind(など)のプロパティを付けてタグにします。
タグで分岐する
パターンマッチ
Rustのmatch、TypeScriptのswitchなどでタグを見て分岐します。
タグがあることで条件が単純化します。
データを持つバリアント
付随データを一緒に管理
「カード」ならカード下4桁、「エラー」ならエラーメッセージのように、必要な情報をそのケースにだけ結びつけられます。
全ケースを網羅する
コンパイラに見てもらう
Rustでは未網羅のmatchに警告やエラーが出ます。
TypeScriptでもnever型を使って「すべて扱っているか」を確認できます。
デフォルト分岐を減らす
defaultを安易に使わない
default(または_のような総受け)は便利ですが、新しいバリアントを見落とす原因になります。
基本は各ケースを明示し、どうしても必要なときだけdefaultを使います。
コード例で学ぶ
Rustの例
支払い方法の分岐
enum Payment {
Cash,
Card { last4: String },
}
fn describe(p: Payment) -> String {
match p {
Payment::Cash => "現金で支払います".to_string(),
Payment::Card { last4 } => format!("カード(下4桁 {})で支払います", last4),
}
}
fn main() {
let a = Payment::Cash;
let b = Payment::Card { last4: "1234".into() };
println!("{}", describe(a));
println!("{}", describe(b));
}
バリアントごとに必要な情報だけを持てるので、不正な組み合わせが入り込みにくくなります。
TypeScriptの例
判別可能なユニオン
type Payment =
| { kind: "Cash" }
| { kind: "Card"; last4: string };
function describe(p: Payment): string {
switch (p.kind) {
case "Cash":
return "現金で支払います";
case "Card":
return `カード(下4桁 ${p.last4})で支払います`;
// defaultは書かずに、全ケースを明示するのがおすすめ
}
}
kindがタグの役割を果たします。
新しいバリアントを追加すると、未対応の分岐に気づけます。
UI状態の管理
状態をタグ付き共用体で表す
読み込み中/成功/失敗/空などのUI状態は、タグ付き共用体と相性が良いです。
type LoadState<T> =
| { kind: "Loading" }
| { kind: "Loaded"; data: T }
| { kind: "Error"; message: string }
| { kind: "Empty" };
function renderUser(state: LoadState<{ name: string }>): string {
switch (state.kind) {
case "Loading":
return "読み込み中…";
case "Loaded":
return `ようこそ、${state.data.name}さん`;
case "Error":
return `エラー: ${state.message}`;
case "Empty":
return "データがありません";
}
}
状態が1つに限定されるため、if文とフラグの組み合わせよりも明快です。
エラー処理の実装
RustのResult型で安全に扱う
fn parse_age(s: &str) -> Result<u32, String> {
match s.parse::<u32>() {
Ok(n) if n <= 150 => Ok(n),
Ok(_) => Err("年齢が不正です".to_string()),
Err(_) => Err("数値に変換できません".to_string()),
}
}
fn main() {
match parse_age("42") {
Ok(age) => println!("年齢: {}", age),
Err(msg) => eprintln!("エラー: {}", msg),
}
}
成功(Ok)と失敗(Err)を型で表すことで、例外に頼らない明確なフローになります。
TypeScriptで簡易Resultを作る
type Result<T, E> =
| { kind: "Ok"; value: T }
| { kind: "Err"; error: E };
function parseAge(s: string): Result<number, string> {
const n = Number(s);
if (!Number.isFinite(n)) return { kind: "Err", error: "数値に変換できません" };
if (n < 0 || n > 150) return { kind: "Err", error: "年齢が不正です" };
return { kind: "Ok", value: Math.floor(n) };
}
const r = parseAge("42");
switch (r.kind) {
case "Ok":
console.log("年齢:", r.value);
break;
case "Err":
console.error("エラー:", r.error);
break;
}
エラー情報も成功結果も、どちらも型安全に受け取れるため、呼び出し側での取り扱いが明確になります。
まとめ
タグ付き共用体(Tagged Union)は、複数の形を取るデータを「タグ付き」で安全に表す仕組みです。
タグにより分岐が分かりやすくなり、網羅性チェックでミスを防げます。
nullに依らない設計やエラー処理にも強く、UI状態などの表現にも向いています。
初心者の方は、まずは小さなドメイン(支払い方法、UIの読み込み状態、成功/失敗の結果など)から試し、「各ケースをタグで明示し、全てのケースを必ず扱う」という基本を身につけていくと、コードの読みやすさと安全性が大きく向上します。
