TypeScript | 基礎文法:Union・基本型操作 – エラーにならないunion設計

TypeScript
スポンサーリンク

なぜ「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 にも idname がある
  • そしてどちらでも 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);
  }
}
TypeScript

union 型に対して「エラーにならないように頑張る」のではなく、

「判別用のキーを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);
  }
}
TypeScript

renderMessage は、「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 を設計していけば、
型エラーは「邪魔者」ではなく、
「設計の甘さを教えてくれる、かなり優秀なレビュー担当」になってくれます。

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