こんにちは、かつコーチです。
前回は any・unknown・never という特殊な型を解説しました。
今回は、複数の型を組み合わせて新しい型を作る「union型」と「intersection型」を扱います。
実務のコードでは「AかBのどちらか」「AとBの両方」という表現が頻繁に必要になるので、ここで基礎をしっかり固めておきましょう。
union型:「AかBのどちらか」を表す型
union型の基本
union型(合併型)は、|(パイプ)で複数の型をつなぎ、「そのうちのどれか一つ」を表す型です。
let id: number | string;
id = 123; // OK
id = "abc-123"; // OK
id = true; // エラー:number型でもstring型でもない
number | string と書くことで、「数値か文字列のどちらか」という型を表現できます。
IDが数値の場合と文字列の場合の両方があるシステムなどで、よく使われる書き方です。
リテラル型のunionでパターンを限定する
union型は、決まった選択肢の中から一つを選ばせたい場合にも便利です。
type Status = "loading" | "success" | "error";
function showMessage(status: Status): string {
if (status === "loading") return "読み込み中です";
if (status === "success") return "成功しました";
return "エラーが発生しました";
}
console.log(showMessage("success")); // "成功しました"
console.log(showMessage("done")); // エラー:"done"はStatus型に含まれない
"loading" | "success" | "error" のように文字列のリテラル型を組み合わせることで、「この3つの値以外は許可しない」という制約をつけられます。
タイプミスや想定外の値の混入を、コンパイル時に防げるのが大きなメリットです。
union型を使うときの型の絞り込み
プロパティが型ごとに異なる場合
union型を使うと、型によって使えるプロパティが異なる場合があります。
type Dog = {
kind: "dog";
bark: () => void;
};
type Cat = {
kind: "cat";
meow: () => void;
};
type Animal = Dog | Cat;
function makeSound(animal: Animal): void {
if (animal.kind === "dog") {
animal.bark(); // ここではanimalがDog型だと確定している
} else {
animal.meow(); // ここではanimalがCat型だと確定している
}
}
Dog と Cat はどちらも kind という共通のプロパティを持ちますが、値がそれぞれ "dog" と "cat" に固定されています。
このように、共通の目印になるプロパティ(kind)を用意しておく設計を「タグ付きunion(判別可能なunion)」と呼びます。
if (animal.kind === "dog") のチェックを通過したブロックでは、TypeScriptが自動的に animal を Dog 型として扱ってくれるため、bark() を安全に呼び出せます。
intersection型:「AとBの両方」を表す型
intersection型の基本
intersection型(交差型)は、&(アンパサンド)で複数の型をつなぎ、「両方の型を同時に満たす」型を表します。
type Named = {
name: string;
};
type Aged = {
age: number;
};
type Person = Named & Aged;
const person: Person = {
name: "かつコーチ",
age: 30,
}; // name と age の両方が必須
Named & Aged と書くと、name と age の両方のプロパティを持つオブジェクトでなければならない、という型になります。
どちらか一方でも欠けているとコンパイルエラーになります。
複数の型を合成して再利用する
intersection型は、既存の型を組み合わせて新しい型を作る場面でよく使われます。
type BaseUser = {
id: number;
name: string;
};
type WithTimestamps = {
createdAt: Date;
updatedAt: Date;
};
type UserWithTimestamps = BaseUser & WithTimestamps;
const user: UserWithTimestamps = {
id: 1,
name: "かつコーチ",
createdAt: new Date(),
updatedAt: new Date(),
};
BaseUser と WithTimestamps という小さな型のパーツを用意しておき、& で組み合わせて UserWithTimestamps という新しい型を作っています。
共通で使う「作成日時・更新日時」のような部分をパーツ化しておくと、複数の型で使い回せて管理がしやすくなります。
つまずきやすいポイント:union型とintersection型を混同する
実際につまずいた話
私がunion型とintersection型を学び始めたころ、「AかBのどちらか」を表現したいのに & を使ってしまい、意図しないエラーに悩まされたことがあります。
やりたかったのは「決済方法として、クレジットカード情報かコンビニ決済情報のどちらかを受け取る」という型でした。
& でつないでしまったせいで、「クレジットカード情報とコンビニ決済情報の両方のプロパティを同時に満たすオブジェクト」という、実際には作れないような型を要求してしまっていました。
エラーメッセージを読んでもすぐには理解できず、「なぜどちらの決済方法を渡してもエラーになるんだろう」としばらく悩んだのを覚えています。
| は「型の選択肢を広げる(どちらかでよい)」、& は「型の制約を狭める(両方満たす必要がある)」という、正反対の性質を持つことを体で理解できた出来事でした。
Before/Afterで確認する
❌ Before:&(intersection型)で「どちらか」を表現しようとする
type CreditCardPayment = {
method: "credit_card";
cardNumber: string;
};
type ConvenienceStorePayment = {
method: "convenience_store";
storeCode: string;
};
// 誤り:&でつなぐと両方のプロパティを同時に満たす必要が出てしまう
type Payment = CreditCardPayment & ConvenienceStorePayment;
const payment: Payment = {
method: "credit_card", // エラー:"credit_card"と"convenience_store"を同時に満たせない
cardNumber: "1234-5678",
storeCode: "A001",
};
method プロパティの型が "credit_card" と "convenience_store" という別々のリテラル型で定義されているため、& で結合すると両方の値を同時に満たす型(実質的に作成不可能な型)になってしまいます。
✅ After:|(union型)で「どちらか」を表現する
type CreditCardPayment = {
method: "credit_card";
cardNumber: string;
};
type ConvenienceStorePayment = {
method: "convenience_store";
storeCode: string;
};
type Payment = CreditCardPayment | ConvenienceStorePayment;
function processPayment(payment: Payment): void {
if (payment.method === "credit_card") {
console.log(`カード番号${payment.cardNumber}で決済します`);
} else {
console.log(`店舗コード${payment.storeCode}で決済します`);
}
}
const payment: Payment = {
method: "credit_card",
cardNumber: "1234-5678",
}; // OK:CreditCardPayment型として成立している
| でつなぐことで、「クレジットカード情報かコンビニ決済情報のどちらか一方」という正しい型を表現できました。
「複数の選択肢の中から一つ」なら |、「複数の条件をすべて満たす」なら &、と覚えておくと迷いにくくなります。
応用:union型とintersection型を組み合わせる
共通部分と個別部分を分けて設計する
実務では、union型とintersection型を組み合わせて設計することもよくあります。
type BasePayment = {
amount: number;
paidAt: Date;
};
type CreditCardDetail = {
method: "credit_card";
cardNumber: string;
};
type ConvenienceStoreDetail = {
method: "convenience_store";
storeCode: string;
};
type Payment = BasePayment & (CreditCardDetail | ConvenienceStoreDetail);
const payment: Payment = {
amount: 5000,
paidAt: new Date(),
method: "credit_card",
cardNumber: "1234-5678",
};
「すべての決済方法に共通する情報(BasePayment)」と「決済方法ごとに異なる情報(CreditCardDetail または ConvenienceStoreDetail)」を分けて定義し、& と | を組み合わせて最終的な型を作っています。
このように型のパーツを目的ごとに小さく分けて組み合わせる設計は、TypeScriptらしい型定義の考え方の一つです。
まとめ
この記事のポイント
- union型(
|)は「複数の型のうちどれか一つ」を表す - intersection型(
&)は「複数の型をすべて満たす」ことを表す - タグ付きunionを使うと、共通プロパティの値で型を安全に絞り込める
- 「どちらか」を
&で、「両方」を|で書くと成立しない型になりやすいので注意する - union型とintersection型は組み合わせて、共通部分と個別部分を分けた設計にも使える
次に読むべき記事
union型とintersection型が分かったら、次は特定の値だけを許可する「リテラル型」をさらに深く見ていきましょう。
→ 次の記事:リテラル型で値を限定する