なぜ「union型なのにすぐエラーになる」のか
まず、よくあるつまずきから整理します。
function printId(id: string | number) {
console.log(id.toUpperCase()); // エラー
}
TypeScript「string | number にしただけなのに、すぐ怒られるじゃん…」
ここで起きていることを一言で言うと、
「union型のままでは、“両方に共通して安全なこと”しかしてはいけない」
というルールに引っかかっている、です。
string には toUpperCase があるけど、number にはない。
だから「今ここで toUpperCase を呼んでいい」とは TypeScript は言えない。
この感覚を押さえたうえで、
「じゃあ、どういう union の設計なら“扱いやすくて、エラーになりにくい”のか?」
を、具体例を通して説明していきます。
ポイント1:union型は「なんでもアリ」ではなく「許可リスト」
「この型“も”OK」「この型“も”OK」を足しているだけ
まず、union型そのものの意味をもう一度整理します。
type Id = string | number;
TypeScriptこれは、
- string というパターンの値もOK
- number というパターンの値もOK
という意味であって、「string と number の“共通項”だけの型」ではありません。
したがって、
function useId(id: Id) {
// id は string かもしれないし number かもしれない
}
TypeScriptという状態になります。
ここで TypeScript は、
- “どちらにも共通して存在する”ものだけなら安全
- “片方にしかない”ものは危険(エラー)
と判断します。
重要なのは、「union型=ゆるくする」ではなく「この候補のどれかだけという“許可リスト”」だということ。
その候補のうちどれなのか確定させない限り、「共通部分」以外は触れない、というルールが効いてきます。
ポイント2:「共通しているものだけ」を正しく見極める
共通プロパティ+共通メソッドだけならそのまま使える
次のような union 型を考えます。
type User = {
id: number;
name: string;
};
type Admin = {
id: number;
name: string;
permissions: string[];
};
type Person = User | Admin;
TypeScriptこのとき、Person に対して次はすべてOKです。
function printBasic(person: Person) {
console.log(person.id); // OK
console.log(person.name); // OK
}
TypeScriptなぜかというと、
UserにもAdminにもidとnameがある- そしてどちらでも
id: number,name: stringで型が一致している
からです。
逆に、
function printPermission(person: Person) {
console.log(person.permissions); // エラー
}
TypeScriptこれは、「User には permissions がない」ので危険、と判断されます。
ここから言える設計ポイントは、
「union に含まれるすべての型に共通して存在するプロパティ・メソッド」だけを、“何も考えずに直で使ってよい領域”として意識する
ということです。
共通じゃないものに触りたいなら、「今どの型か」をはっきりさせる必要が出てきます(後述)。
ポイント3:「判別用プロパティ」を1つ持たせると一気に楽になる
Discriminated Union にしてしまう
さきほどの Person を、判別用プロパティ付きに変えてみます。
type User = {
kind: "user";
id: number;
name: string;
};
type Admin = {
kind: "admin";
id: number;
name: string;
permissions: string[];
};
type Person = User | Admin;
TypeScriptここで kind が共通プロパティです。
この形(Discriminated Union)にしておくと、if や switch で分岐したときに型の絞り込みが自然に働きます。
function printPerson(person: Person) {
if (person.kind === "admin") {
// ここでは person は Admin 型に絞られる
console.log(person.name, person.permissions);
} else {
// ここでは person は User 型
console.log(person.name);
}
}
TypeScriptunion 型に対して「エラーにならないように頑張る」のではなく、
「判別用のキーを1つ決めて、その値で分ければ、各ケースで“その型にだけあるプロパティ”を普通に使えるように設計してしまう」
という発想がめちゃくちゃ重要です。
「いくつかのパターンがある」+「そのパターンごとに持つ情報が違う」
という場面では、最初から Discriminated Union を意識すると、
エラーと戦う時間が一気に減ります。
ポイント4:unionを受け取る関数は「責任範囲」を決める
その関数は「どこまで面倒を見るべきか」を決める
例えば、次のような型があったとします。
type TextMessage = {
type: "text";
text: string;
};
type ImageMessage = {
type: "image";
url: string;
};
type Message = TextMessage | ImageMessage;
TypeScriptここで、全メッセージを表示する renderMessage を考えます。
function renderMessage(msg: Message) {
if (msg.type === "text") {
console.log("TEXT:", msg.text);
} else {
console.log("IMAGE:", msg.url);
}
}
TypeScriptrenderMessage は、「Message のすべてのケースを面倒見る関数」です。
その分、関数の中にちゃんと分岐を書かないといけません。
一方で、「テキストメッセージだけを処理したい」関数なら、そもそも union を受ける必要はありません。
function printText(msg: TextMessage) {
console.log(msg.text);
}
TypeScriptここでの大事な視点は、
「この関数は、どの範囲までのパターンを受け取るべきか?」
をちゃんと設計する、ということです。
なんでもかんでも Message(union)を引数にすると、その関数の中で毎回「全部のケースに対応しなきゃいけない」義務を背負うことになります。
逆に、
- 「ここはテキストだけ」「ここは画像だけ」を扱う関数
- 「どれが来てもよし、全部を面倒見る」関数
を分けておくと、後者にだけ union を使えば良くなり、エラーの発生箇所も論理的に限られます。
ポイント5:「型ガードで絞ってから使う」を型の前提にする
unionの値を渡すなら、「中で型ガードする設計」をセットで考える
「エラーにならない union 設計」とは、裏を返すと、
「union 型を受け取るときは、“必ずどこかで型を絞る”という前提で関数を設計する」
ということです。
たとえば、
type Id = string | number;
function printId(id: Id) {
if (typeof id === "string") {
console.log(id.toUpperCase());
} else {
console.log(id.toFixed(2));
}
}
TypeScriptこの関数は、「Id の両方のパターン(string / number)を責任もって扱う」関数です。
だから中で絞り込み(narrowing)を行っています。
逆に、もし「ここは string のときだけでいい」と割り切れるなら、
最初から引数の型を string にしてしまって構いません。
「union を受け取る関数」=「複数パターンを分岐する責任を持った関数」
という捉え方ができていれば、
エラーは「分岐を忘れているところを教えてくれるチェック」になります。
ポイント6:雑な union を作らない(“とりあえず | で繋がない”)
A か B か C かよく分からない union は、必ず自分の首を絞める
ありがちな失敗として、
type Something = string | number | boolean | null;
TypeScriptみたいな「なんでもあり」な union をサクッと作ってしまうパターンがあります。
これはほぼ確実に、あとで扱いづらくなります。
なぜなら、
- 関数の中で「全部のケース」を分けて扱おうとすると if 地獄になる
- 共通プロパティもほぼないので、できることがほとんど増えない
- 「この値は本当は何のつもりなのか」が型から読めない
からです。
エラーにならない union 設計のためには、
- union に含める型は「意味のある少数のパターン」に絞ること
- 「そのパターンごとに何をしたいか」を最初からイメージしておくこと
がとても重要です。
「とりあえず | で繋いでおくか」は、将来の自分への負債になりやすい。
union を作るときは、“この2〜3パターンだけ”と、ちゃんと意味のある選択肢に絞る。
これが、エラーと戦わないための実は一番効くコツです。
まとめ:エラーにならないunion設計の「考え方」を自分の言葉で言うと
ここまでのポイントを、あなた自身の言葉にするとこうなります。
- union は「どれが来てもよい」のではなく、「この候補のどれかだけ」という“許可リスト”
- 何も分岐しないまま触っていいのは、「全候補に共通している安全な部分」だけ
- パターンごとに持つ情報が違うなら、判別用プロパティ(status / type / kind)を1つ決めて Discriminated Union にする
- union を受け取る関数は、「中でちゃんと型を絞る責任を持つ関数」に限る
- 「とりあえず | で繋ぐ」ような雑な union は作らない。パターンを意識して絞る
そして、union を使う前に必ず自分に聞いてみてほしいことがあります。
「この union に含めているパターンは、本当に“全部”必要?」
「この関数は、その全部のパターンを扱う責任を、本当に持つべき?」
ここで一呼吸おいて考えたうえで union を設計していけば、
型エラーは「邪魔者」ではなく、
「設計の甘さを教えてくれる、かなり優秀なレビュー担当」になってくれます。
