第88章:Visitor ② TSの現実解:判別Union+関数で“訪問”する🧠
ねらい🎯
- 「構造(データの形)」は固定のまま、「処理(表示・集計・検証)」をどんどん足せるようになる✨
- GoFのVisitorを TypeScriptっぽく “軽量”に再現できるようになる🧁
switchの 網羅チェック(漏れたらコンパイルで気づく)を体に入れる✅ (TypeScript)
まず困りごと😵💫:「同じデータに処理を足すたび、あちこちを編集…」
例:メニューの木構造(Composite🌳)があって…
- 画面表示したい🖥️
- 合計金額を出したい🧾
- 入力データを検証したい🔍
…となるたびに、いろんな場所の if/switch を増やすと、すぐ “編集漏れ” が起きる😭
そこで「処理の追加を1か所に集める」発想がVisitor✨
TSでの現実解🧠:GoFの“クラスVisitor”より、判別Union+関数が自然
TypeScriptは「データの形」を 判別Union(discriminated union)で表すのが強い💪
type(or kind)みたいな タグで分岐すると、型がちゃんと絞り込まれるよ🧠 (TypeScript)
そしてVisitorはこうする👇
- ノード(データ):判別Union
- Visitor(処理の集合):
{ タグ名: (node)=>結果 }のオブジェクト - 入口:
visit(node, visitor)を1つだけ用意

ハンズオン🛠️:カフェの「メニュー木」を“訪問”して、表示/集計/検証を足していく☕🌳
1) データ構造:判別Unionで「Leaf/Group」を表現🌿🌲
// menu.ts
export type MenuNode =
| { type: "item"; id: string; name: string; price: number }
| { type: "group"; id: string; name: string; children: MenuNode[] };
type がタグになって、switch で絞り込みできるよ✨ (TypeScript)
2) Visitor型:タグごとに“処理”を持つ🧳✨
ポイントはここ👇
Extract<Union, {type:"..."}>で「そのタグのノード型だけ」を抜き出すsatisfiesで「漏れなし&余計なキーなし」をチェック✅(超便利!) (effectivetypescript.com)
// visitor.ts
import { MenuNode } from "./menu";
type NodeOf<TType extends MenuNode["type"]> = Extract<MenuNode, { type: TType }>;
export type Visitor<R> = {
item: (node: NodeOf<"item">) => R;
group: (node: NodeOf<"group">) => R;
};
3) visit関数:入口はここ1か所🚪✨(網羅チェックつき)
// visit.ts
import { MenuNode } from "./menu";
import { Visitor } from "./visitor";
function assertNever(x: never): never {
throw new Error("Unexpected node: " + JSON.stringify(x));
}
export function visit<R>(node: MenuNode, v: Visitor<R>): R {
switch (node.type) {
case "item":
return v.item(node);
case "group":
return v.group(node);
default:
// ここに来たら「タグ追加したのに処理書いてない」ってこと!
return assertNever(node);
}
}
never を使うと switchの漏れ をコンパイルで検出できる✅ (TypeScript)
4) Visitorを作る①:表示用(メニューを文字列で整形)🖊️🍰
// renderVisitor.ts
import { Visitor } from "./visitor";
import { visit } from "./visit";
import { MenuNode } from "./menu";
export const renderVisitor = {
item: (n) => `• ${n.name} ¥${n.price}`,
group: (n) => {
const lines = n.children.map((c) => visit(c, renderVisitor));
return `【${n.name}】\n` + lines.map((s) => " " + s.replace(/\n/g, "\n ")).join("\n");
},
} satisfies Visitor<string>; // ✅ 漏れ・余計キーをチェック
satisfies は「型を満たすか検証しつつ、値の型情報も保ちやすい」用途でよく使われるよ🧠✨ (effectivetypescript.com)
5) Visitorを作る②:合計金額(木を全部足す)🧾✨
// totalVisitor.ts
import { Visitor } from "./visitor";
import { visit } from "./visit";
export const totalVisitor = {
item: (n) => n.price,
group: (n) => n.children.reduce((sum, c) => sum + visit(c, totalVisitor), 0),
} satisfies Visitor<number>;
6) 実行してみよ🎉
// demo.ts
import { MenuNode } from "./menu";
import { visit } from "./visit";
import { renderVisitor } from "./renderVisitor";
import { totalVisitor } from "./totalVisitor";
const menu: MenuNode = {
type: "group",
id: "root",
name: "メインメニュー",
children: [
{ type: "item", id: "c1", name: "コーヒー", price: 450 },
{ type: "item", id: "t1", name: "紅茶", price: 420 },
{
type: "group",
id: "sweets",
name: "スイーツ",
children: [{ type: "item", id: "ck", name: "チーズケーキ", price: 520 }],
},
],
};
console.log(visit(menu, renderVisitor));
console.log("合計:", visit(menu, totalVisitor));
✅ “構造”はそのまま、Visitorを増やすだけで“処理”が増える!最高👏✨
よくある拡張シーン🍀(Visitorが効くやつ)
- 表示形式を増やす:テキスト / HTML / JSON など🧩
- 集計を増やす:合計金額 / 件数 / 最安値 / カテゴリ別合計📊
- 検証を増やす:価格がマイナスじゃない? id重複してない?🔍
つまずき回避💡(TSでVisitorやるときの“あるある”)
1) 「Visitorの中で再帰したいのに、参照が難しい」問題🌀
上の例みたいに visitorオブジェクト自身を参照して再帰するのが一番シンプル👍
(function makeVisitor(){...} にしてクロージャで持ってもOKだよ🧁)
2) 「タグ増やしたのに、どこか直し忘れた😇」
visitのswitchにassertNeverを入れる✅ (TypeScript)- visitor定義に
satisfies Visitor<...>を付ける✅ (effectivetypescript.com) この2枚看板で“漏れ”を潰せるよ💥
3) ファイルが増えて迷子になる📁💦
おすすめルール(最小)👇
menu.ts(型)visit.ts(入口)visitors/xxxVisitor.ts(処理たち) これだけでかなりスッキリ✨
ミニコラム🗞️:最近のTypeScript事情(“大きいUnion”が現実になる時代)
- 2026/02/05時点のTypeScript安定版は 5.9.3(公式リリース一覧ベース)📌 (GitHub)
- 判別Unionは便利だけど、巨大になると型計算コストが気になることも。TypeScript 5.5では 判別Union由来の型計算をキャッシュして高速化みたいな改善も入ってるよ⏱️✨ (TypeScript)
- さらに、コンパイラ/言語サービスの ネイティブ移植(TypeScript Native Previews) も進んでいて、
@typescript/native-previewで試せる流れがあるよ🚀 (Microsoft for Developers) (Visitorみたいに「型で安全に増やす」設計は、こういう流れとも相性いい💞)
AIプロンプト例🤖💬(Visitorを“読みやすく”したい時)
判別Unionで表したツリー構造があります。TypeScriptでVisitorを「関数オブジェクト + visit関数」で実装してください。
条件:
- switchの網羅チェック(never/assertNever)を入れる
- visitor定義は `satisfies` で漏れを検出できるようにする
- クラスでGoF Visitorを再現しない(TSらしく関数で)
- 例として「表示Visitor」「合計Visitor」「検証Visitor(エラー配列を返す)」の3つを作る
出力: 設計の意図 → 最小コード → ありがちなミス3つ
satisfies と never をちゃんと入れてくるかをチェックすると、AIの“それっぽい”事故が減るよ✅✨ (effectivetypescript.com)