TypeScriptにおける型システムの柔軟性と安全性を象徴する機能が「Union(ユニオン)型」です。
JavaScriptが持つ動的な性質を活かしつつ、コンパイル時に厳密なチェックを行う機能になります。
本記事では、基本構文から実務で必須となる「型ガード(Type Guards)」、高度な設計手法である「判別可能なユニオン」まで解説します。
Union(ユニオン型)とは?
プログラミングにおいて、「この変数は文字列が入るかもしれないし、数値が入るかもしれない」という状況は頻繁に発生します。
例えば、APIから取得するデータが「成功時はデータオブジェクト、失敗時はエラーメッセージの文字列」となるケースなどです。
他言語では、これを表現するために複雑なクラス設計が必要になることがありますが、TypeScriptにはシンプルで強力な方法が用意されています。
それが「Union(ユニオン)型」です。
Union型は、「A または B」という複数の型の可能性を一つの型として表現する機能です。
これにより、JavaScript特有の「一つの変数が様々な型の値を持つ」という動的な挙動をTypeScriptの型システムの中に安全に組み込むことができます。
unionの基本構文と使い方
Union型の最も基本的な書き方から見ていきましょう。
- 複数の型を許容する
|(パイプ) の役割 - 変数、関数の引数・戻り値での実践例
複数の型を許容する | (パイプ) の役割
Union型を定義するには、許容したい型を |(パイプライン演算子)で繋ぎます。
英語の「OR(または)」に相当すると考えると理解しやすいです。
// value は string 型 または number 型 を受け入れる
let value: string | number;
value = "Hello"; // OK
value = 100; // OK
// value = true; // NG: boolean は指定されていないためコンパイルエラーany型を使って型安全性を完全に放棄することなく、特定の複数の型だけを許可することができます。
変数、関数の引数・戻り値での実践例
実務において、Union型は関数の引数や戻り値で最も頻繁に利用されます。
例えば、引数として「文字列」または「文字列の配列」を受け取り、すべて結合して出力する関数を考えてみましょう。
// 引数 text は string または string[] のどちらか
function formatText(text: string | string[]): string {
// 注意:この時点では、text が string か string[] か確定していません
// そのため、text.join() などを直接呼ぶとエラーになります
if (Array.isArray(text)) {
// ここでは text は string[] として扱われる
return text.join(", ");
} else {
// ここでは text は string として扱われる
return text.trim();
}
}
console.log(formatText(" Apple ")); // 出力: "Apple"
console.log(formatText(["Apple", "Pen"])); // 出力: "Apple, Pen"この例で重要なのは、関数内部のif (Array.isArray(text))という分岐です。
TypeScriptのコンパイラは賢く、条件分岐を読み取って「ブロックの中でtextは配列だ」「elseの中で文字列だ」と型を自動的に絞り込んでくれます。
unionを安全に扱う「型ガード(Type Guards)」
Union型(例:A | B)の変数に対して、AとBの両方に共通して存在するプロパティやメソッドにしかそのままではアクセスできません。
どちらか一方にしか存在しない機能を使うためには、「現在どちらの型であるか」を特定する必要があります。
この絞り込みの仕組みを「型ガード(Type Guards)」と呼びます。
typeofを用いたプリミティブ型の絞り込みin演算子によるオブジェクト型の絞り込み- ユーザー定義型ガード(
is演算子)
typeofを用いたプリミティブ型の絞り込み
string,number,booleanなどのプリミティブ型がUnionで組み合わされている場合、JavaScriptのtypeof演算子が型ガードとして機能します。
function printId(id: number | string) {
if (typeof id === "string") {
// このブロック内では id は string 型
console.log(`ID is a string: ${id.toUpperCase()}`);
} else {
// このブロック内では id は number 型
console.log(`ID is a number: ${id.toFixed(2)}`);
}
}in演算子によるオブジェクト型の絞り込み
独自のインターフェース同士のUnionの場合は、typeofはすべて"object"を返してしまうため使えません。
代わりに、特定のプロパティがオブジェクトに存在するかをチェックするin演算子を使用します。
interface Bird {
fly: () => void;
layEggs: () => void;
}
interface Fish {
swim: () => void;
layEggs: () => void;
}
type Pet = Bird | Fish;
function movePet(pet: Pet) {
// pet.fly() と直接書くと、Fish だった場合にエラーになるためコンパイルが通らない
// 両方に存在する layEggs() なら呼べる
if ("fly" in pet) {
// "fly" プロパティを持つのは Bird なので、ここは Bird 型に絞り込まれる
pet.fly();
} else {
// それ以外は Fish 型に絞り込まれる
pet.swim();
}
}ユーザー定義型ガード(is演算子)
判定ロジックが複雑な場合、判定用の関数を別に切り出すことができます。
その際、戻り値の型に引数名 is 型名という特殊な記法(型述語)を使用します。
// 戻り値の型に `pet is Fish` と記述する
function isFish(pet: Pet): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function handlePet(pet: Pet) {
if (isFish(pet)) {
// ここで pet は Fish 型になる
pet.swim();
} else {
// ここで pet は Bird 型になる
pet.fly();
}
}判別可能なユニオン(Discriminated Unions)
in演算子やユーザー定義型ガードは便利ですが、扱うオブジェクトの種類が3つ4つと増えると、分岐のコードが複雑で読みにくくなります。
TypeScript開発において、複雑な状態管理やAPIレスポンスの処理でデファクトスタンダード(標準)となっている最強のパターンが「判別可能なユニオン」です。
- 通常のオブジェクトUnionにおける課題
- 共通のプロパティ(タグ)を持たせて型を特定する
- APIレスポンスやReactのState管理での実践例
通常のオブジェクトUnionにおける課題
例えば、決済方法として「クレジットカード」「PayPay」「銀行振込」の3種類のオブジェクトがあるとします。
それぞれ持っているプロパティがバラバラです。
これをin演算子で絞り込むのは至難の業です。
共通のプロパティ(タグ)を持たせて型を特定する
「判別可能なユニオン」のルールは非常にシンプルです。
結合する全てのオブジェクト型に、共通のプロパティ(typeやkindという名前の文字列リテラル型)を持たせるだけです。
// 各インターフェースに、固定の文字列を持つ 'type' プロパティを含める
interface CreditCardPayment {
type: "CREDIT_CARD"; // これがタグになる
cardNumber: string;
securityCode: string;
}
interface PayPayPayment {
type: "PAYPAY";
phoneNumber: string;
}
interface BankTransferPayment {
type: "BANK_TRANSFER";
bankName: string;
accountNumber: string;
}
// 判別可能なユニオン型の定義
type PaymentMethod = CreditCardPayment | PayPayPayment | BankTransferPayment;このようにタグ(目印)を仕込むと、switch文やif文でtypeプロパティをチェックするだけでTypeScriptは型を絞り込んでくれます。
function processPayment(payment: PaymentMethod) {
switch (payment.type) {
case "CREDIT_CARD":
// ここでは CreditCardPayment 型に確定している!
console.log(`カード番号: ${payment.cardNumber} で決済します`);
break;
case "PAYPAY":
// ここでは PayPayPayment 型に確定している!
console.log(`電話番号: ${payment.phoneNumber} に請求します`);
break;
case "BANK_TRANSFER":
// ここでは BankTransferPayment 型に確定している!
console.log(`${payment.bankName} への振込を確認します`);
break;
}
}APIレスポンスやReactのState管理での実践例
このパターンは、ReactやReduxなどでの状態(State)管理と相性が良いです。
例えば、データの取得状態を管理する場合、それぞれ独立したフラグを持つ設計(悪い例)と、判別可能なユニオンを用いた設計(良い例)を比較してみましょう。
interface State {
isLoading: boolean;
error: string | null;
data: any | null;
}
// isLoading が false なのに error と data の両方に値が入る、といった矛盾した状態を作れてしまう。type FetchState<T> =
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
// 状態が "success" の時にしか data プロパティは存在しないため、
// 矛盾した状態を型レベルで完全に排除できる!unionと組み合わせて使うテクニック
Union型をマスターしたエンジニアが次に覚えるべき、品質を担保するためのテクニックを紹介します。
never型を用いた網羅性チェック- 交差型(Intersection Types
&)との違い
never型を用いた網羅性チェック
switch文を用いた判別可能なユニオンの処理において、「将来、新しい決済方法(例:"BITCOIN")が追加されたときに、switch文の分岐を追加し忘れたらどうなるか?」という問題があります。
これをコンパイルエラーとして検知させるのが 「never型を用いた網羅性チェック」です。
function processPaymentSafe(payment: PaymentMethod) {
switch (payment.type) {
case "CREDIT_CARD":
// 処理...
return;
case "PAYPAY":
// 処理...
return;
case "BANK_TRANSFER":
// 処理...
return;
default:
// ここがポイント!すべてのケースを網羅していれば、
// ここに到達した時点での payment の型は `never`(あり得ない型)になります。
const _exhaustiveCheck: never = payment;
throw new Error(`未知の決済方法です: ${_exhaustiveCheck}`);
}
}交差型(Intersection Types &)との違い
Union型(|)とよく比較されるのが交差型(Intersection Types &)です。
- Union (
|): A または B(どちらかの性質を満たす) - Intersection (
&): A かつ B(両方の性質をすべて満たす)
interface HasName { name: string; }
interface HasAge { age: number; }
// Union: どちらかのプロパティがあればOK
type UnionType = HasName | HasAge;
const u1: UnionType = { name: "Alice" }; // OK
// Intersection: 両方のプロパティが必須
type IntersectionType = HasName & HasAge;
const i1: IntersectionType = { name: "Bob", age: 30 }; // OK
// const i2: IntersectionType = { name: "Charlie" }; // NG: ageがない実務では、基礎となる型を交差型(&)で作った上で、それらをUnion型(|)でまとめるといったパズルのような型の組み立てを行います。

