【実務・中級編】 タグ付きユニオン(判別共用体)の模倣 – JavaScript実践ガイド

おい、調子はどうだ?
今日も元気に `Cannot read properties of undefined (reading ‘map’)` と格闘してないか?(笑)

フロントエンドの実務で、TypeScript全盛の時代とはいえ、結局のところビルド後のランタイムや、JSDocで堅牢に書きたいプロジェクト、あるいは純粋なJavaScriptベースの複雑なステート管理に直面したとき、「データの構造に応じた安全な分岐」で頭を悩ませた経験は誰にでもあるはずだ。

「このオブジェクト、いま何のデータが入っているんだ……? `user` なのか、それとも `error` なのか……?」

`if (data.name)` とか `if (‘code’ in data)` みたいなどこか泥臭いプロパティの有無チェックでコードを汚染していくのは、もう終わりにしようぜ。今回は、動的言語であるJavaScriptにおいて、堅牢で予測可能なコードを書くための秘伝のタレ――「タグ付きユニオン(判別共用体)」の模倣について、俺の実務での知見を交えて徹底的に解説してやる。

—

タグ付きユニオン(判別共用体)とは何か?

タグ付きユニオン(Tagged Union / Discriminated Union)とは、複数の異なるデータ構造(オブジェクト)のそれぞれに、共通の識別子(これを「タグ」と呼ぶ)を持たせることで、ランタイムや型システムに「今どの形状のデータを使っているのか」を明確に伝えるデザインパターンだ。

TypeScriptなら `type Result = { tag: ‘success’, data: Data } | { tag: ‘error’, error: Error }` みたいに書けるあれだな。

じゃあ、型チェック機構を持たない素のJavaScript(あるいはJSDoc環境)でこれをどう実現するか?
答えはシンプルだ。「オブジェクトに明示的な `type`(または `kind`)プロプティを持たせ、それを `switch` 文の羅針盤にする」。これだけで、コードの可読性と保守性は劇的に跳ね上がる。

なぜ `typeof` や `in` チェックだけでは不十分なのか?

JavaScriptの標準機能である `typeof` や `instanceof`、あるいは `in` 演算子。これらは便利だが、実務の複雑なドメインモデルを扱うには少し「ボロ」が出る。

例えば、APIから返ってきたレスポンスを処理するシチュエーションを考えてみよう。
ローディング中、成功、失敗の3つの状態があり、それぞれ持つべきプロパティが違うとする。

これを `if (‘error’ in res)` のようなその場しのぎの判定で書いているとどうなるか?
後から「やっぱりローディング中にも進捗率(progress)を持たせたい」となった瞬間、条件分岐のジャングルが崩壊し、どこかで `undefined` の踏み抜き事故が起きる。

ブラウザのJavaScriptエンジン(V8など)の裏側の話もしておこう。
V8などのモダンなJSエンジンは、「隠しクラス(Hidden Class / Shapes)」という仕組みを使ってオブジェクトのプロパティアクセスを最適化している。オブジェクトが持つプロパティの構造(形状)が一致していれば、エンジンは高速なインラインキャッシュを効かせられる。
つまり、データ構造ごとに `type` プロパティを固定化し、形状を綺麗に整えておくことは、人間にとって読みやすいだけでなく、V8エンジンにとっても最適化しやすい(=パフォーマンス上有利な)書き方なのだ。

—

実践!現場で使えるタグ付きユニオンの実装パターン

百聞は一見に如かず。実務でそのまま使える綺麗なコード例を見せよう。
ここでは、ECサイトのカートや非同期通信の状態管理をイメージして、リクエストの状態をタグ付きユニオンで美しく制御するモジュールを書いた。

エディタに貼り付けて、Node.jsやブラウザのコンソールで動かしてみてくれ。

/

  • @file asyncState.js
  • JavaScriptにおけるタグ付きユニオンを使った堅牢な状態管理のサンプル

/

// — 1. ファクトリー関数(データの形状を強制するコンストラクタ) —
// 直接オブジェクトリテラルを書くのもいいが、ファクトリー関数を通すことで
// 「タグの打ち間違え」や「必須プロパティの入れ忘れ」を完全に防げる。

const AsyncState = {
idle: () => ({
type: ‘IDLE’,
}),

loading: (progress = 0) => ({
type: ‘LOADING’,
progress: progress, // 0 〜 100 の進捗率
}),

success: (data) => ({
type: ‘SUCCESS’,
data: data,
}),

failure: (error) => ({
type: ‘FAILURE’,
error: error,
}),
};

// — 2. パターンマッチング(Reducer的な分岐処理)のヘルパー —
// switch文を毎回書くのが面倒な場合、このようなハンドラー関数を用意すると神。

function matchAsyncState(state, handlers) {
const handler = handlers[state.type];

if (!handler) {
// 網羅性チェックの代わり。想定外のタグが来たら即座にエラーを吐かせる。
throw new Error(`未知的化されたステートタイプです: ${state.type}`);
}

// マッチしたハンドラーに、オブジェクト自身をペイロードとして渡す
return handler(state);
}

// — 3. 実際の利用シーン(ビジネスロジック層) —

// 初期状態
let currentState = AsyncState.idle();

// 状態がローディングに変わった
currentState = AsyncState.loading(45);

// 状態に応じたUIの描画やデータの処理を安全に行う
function renderUI(state) {
// matchAsyncState を使えば、if/elseのネスト地獄から解放される
return matchAsyncState(state, {
IDLE: () => ‘まだ処理は始まっていません。’,

LOADING: (s) => `読み込み中… (${s.progress}%)`,

SUCCESS: (s) => {
// ここでは s.data が確実に存在することが保証されている
return `データ取得成功: ${s.data.map(item => item.title).join(‘, ‘)}`;
},

FAILURE: (s) => {
// ここでは s.error が確実に存在し、型安全(気分的に)に扱える
return `エラー発生: ${s.error.message}`;
}
});
}

// 実行テスト
console.log(renderUI(currentState));
// 出力: 読み込み中… (45%)

// 成功データを受け取ったと仮定
currentState = AsyncState.success([
{ id: 1, title: ‘JavaScriptの極意’ },
{ id: 2, title: ‘フロントエンドアーキテクチャ論’ }
]);

console.log(renderUI(currentState));
// 出力: データ取得成功: JavaScriptの極意, フロントエンドアーキテクチャ論

—

シニアから後輩へ送る、実務でのベストプラクティス

このパターンを実際のプロダクトに導入するとき、いくつか意識してほしい「現場の知見」がある。

1. タグは大文字のスネークケース(例: `’FETCH_SUCCESS’`)で統一しろ
文字列の比較ミスを防ぐため、定数(Constant)として定義するか、命名規則を厳格に揃えよう。TypeScriptならユニオン型(リテラル型)と組み合わせることでコンパイルエラーにできるが、JSならファクトリー関数の中に閉じ込めるのがミソだ。
2. オブジェクトのイミュータビリティ(不変性)を保て
タグ付きユニオンの最大のメリットは「状態の予測可能性」だ。`state.data = …` のように直接プロパティを書き換える(ミューテートする)のは絶対にNG。状態が変わるときは、必ず新しいオブジェクト(ファクトリー関数を通したもの)を生成して置き換えろ。
3. 網羅性を担保する `default` の設計
`switch` 文を使う場合は、`default` 節で予期せぬステート(将来追加されるかもしれないステートや不正なデータ)を受け取ったときに `throw new Error` を仕込むか、フォールバック用の処理を必ず書くこと。これにより、「サイレントバグ(エラーを出さずに画面が真っ白になる現象)」を未然に防げる。

—

おわりに

タグ付きユニオンの模倣は、一見すると「ただのオブジェクトと文字列の比較」に見えるかもしれない。だが、この設計思想をチーム全体で共有できると、コードの「カオス度」が劇的に下がる。

「このデータのとき、画面はどうあるべきか」が、コードの構造そのものから透けて見えるようになるんだ。

明日からのコードレビューで、もし `if (res.status === 200 && res.body.data)` みたいな条件分岐の山を見かけたら、ぜひこのパターンの導入をチームに提案してみてくれ。
君の背中を見た後輩たちは、きっと「おっ、こいつの書くコード、なんか一味違うぞ」って一目置くようになるはずさ。

それじゃ、今日も良いコードを書こうぜ!

コメント

タイトルとURLをコピーしました