🔗
TypeScriptの型を集合の観点で理解する - ユニオンとインターセクションを図解で解説
はじめに
TypeScript を学習していると、「型が複雑でよくわからない...」と感じることがありませんか?特に、ユニオン型(|
)やインターセクション型(&
)が出てくると、頭が混乱してしまう方も多いのではないでしょうか。
実は、TypeScript の型を集合(Set)の概念で考えると、これらの複雑な型も直感的に理解できるようになります。
この記事では、初心者の方でもわかりやすく、TypeScript の型システムを集合論の観点から解説していきます。
型を集合として考える基本概念
型 = 値の集合
TypeScript において、**型とは「その型に属する値の集合」**として考えることができます。
// string型 = すべての文字列値の集合
type StringType = string; // "hello", "world", "TypeScript", ... の集合
// number型 = すべての数値の集合
type NumberType = number; // 1, 2, 3.14, -5, ... の集合
// boolean型 = true と false の集合
type BooleanType = boolean; // { true, false } の集合
リテラル型は 1 つの要素を持つ集合
// "hello"型 = "hello"という値のみを持つ集合
type HelloType = "hello"; // { "hello" }
// 42型 = 42という値のみを持つ集合
type FortyTwoType = 42; // { 42 }
// trueリテラル型 = trueのみを持つ集合
type TrueType = true; // { true }
ユニオン型(|)= 和集合
ユニオン型は数学の**和集合(Union)**に対応します。
基本的なユニオン型
// string または number の値を受け入れる
type StringOrNumber = string | number;
// これは以下の集合と同じ意味
// { "hello", "world", ..., 1, 2, 3.14, ... }
視覚的に表現すると:
[string型の集合] ∪ [number型の集合] = [StringOrNumber型の集合]
"hello" ─┐
"world" ─┤
... ─┤ ← string型
├─ StringOrNumber型
1 ─┤
42 ─┤
3.14 ─┤ ← number型
... ─┘
リテラル型のユニオン
// "red", "green", "blue" のいずれかの値のみ受け入れる
type Color = "red" | "green" | "blue";
// 使用例
const primaryColor: Color = "red"; // ✅ OK
const invalidColor: Color = "yellow"; // ❌ Error
実用的なユニオン型の例
// APIの応答を表現
type ApiResponse =
| { status: "success"; data: any }
| { status: "error"; message: string };
// 使用例
function handleResponse(response: ApiResponse) {
if (response.status === "success") {
console.log(response.data); // ここでは data プロパティにアクセス可能
} else {
console.log(response.message); // ここでは message プロパティにアクセス可能
}
}
インターセクション型(&)= 積集合
インターセクション型は数学の**積集合(Intersection)**に対応します。
オブジェクト型のインターセクション
オブジェクト型の場合、インターセクション型は「両方の型のプロパティをすべて持つ型」になります。
interface Person {
name: string;
age: number;
}
interface Employee {
employeeId: string;
department: string;
}
// Person と Employee の両方のプロパティを持つ型
type PersonEmployee = Person & Employee;
// 以下と同じ意味
type PersonEmployee = {
name: string; // Personから
age: number; // Personから
employeeId: string; // Employeeから
department: string; // Employeeから
};
視覚的に表現すると:
Person型のプロパティ Employee型のプロパティ
┌─────────────────┐ ┌─────────────────────┐
│ name: string │ │ employeeId: string │
│ age: number │ │ department: string │
└─────────────────┘ └─────────────────────┘
│ │
└───── 結合(&)──────────┘
│
┌─────────────────────────────┐
│ name: string │
│ age: number │ ← PersonEmployee型
│ employeeId: string │
│ department: string │
└─────────────────────────────┘
実用的なインターセクション型の例
// 基本的なユーザー情報
interface BaseUser {
id: string;
name: string;
email: string;
}
// 権限情報
interface Permissions {
canEdit: boolean;
canDelete: boolean;
canView: boolean;
}
// タイムスタンプ情報
interface Timestamps {
createdAt: Date;
updatedAt: Date;
}
// すべてを組み合わせた完全なユーザー型
type FullUser = BaseUser & Permissions & Timestamps;
const user: FullUser = {
// BaseUser の要求
id: "user123",
name: "田中太郎",
email: "tanaka@example.com",
// Permissions の要求
canEdit: true,
canDelete: false,
canView: true,
// Timestamps の要求
createdAt: new Date("2024-01-01"),
updatedAt: new Date("2024-01-15"),
};
ユニオンとインターセクションの混合理解
オブジェクトのユニオン型(重要!)
オブジェクトのユニオン型は、初心者が混乱しやすい部分です。集合の観点から理解してみましょう。
interface Cat {
type: "cat";
meow(): void;
}
interface Dog {
type: "dog";
bark(): void;
}
type Pet = Cat | Dog;
このPet
型は「Cat
型の値の集合」または「Dog
型の値の集合」のいずれかに属する値を表します:
// 正しい使用例
const cat: Pet = {
type: "cat",
meow() {
console.log("にゃー");
},
}; // ✅ Cat型として有効
const dog: Pet = {
type: "dog",
bark() {
console.log("わんわん");
},
}; // ✅ Dog型として有効
// 間違った使用例
const invalidPet: Pet = {
type: "cat",
bark() {
console.log("わんわん");
}, // ❌ Error: Cat型にbarkは存在しない
};
判別可能なユニオン(Discriminated Union)
共通のプロパティを使って型を判別する手法です:
function handlePet(pet: Pet) {
if (pet.type === "cat") {
pet.meow(); // TypeScriptが pet を Cat型と推論
} else {
pet.bark(); // TypeScriptが pet を Dog型と推論
}
}
実践的な型設計パターン
パターン 1:設定オブジェクトの型設計
// 基本設定
interface BaseConfig {
apiUrl: string;
timeout: number;
}
// 開発環境用の追加設定
interface DevConfig {
debug: boolean;
logLevel: "info" | "debug" | "error";
}
// 本番環境用の追加設定
interface ProdConfig {
analytics: boolean;
errorReporting: boolean;
}
// 環境ごとの設定型
type DevEnvironment = BaseConfig & DevConfig;
type ProdEnvironment = BaseConfig & ProdConfig;
// アプリケーション全体の設定
type AppConfig = DevEnvironment | ProdEnvironment;
パターン 2:API レスポンスの型設計
// 成功時の基本構造
interface SuccessResponse<T> {
success: true;
data: T;
timestamp: string;
}
// エラー時の基本構造
interface ErrorResponse {
success: false;
error: {
code: string;
message: string;
};
timestamp: string;
}
// ユーザー情報API
type UserResponse = SuccessResponse<User> | ErrorResponse;
// 記事一覧API
type ArticleListResponse = SuccessResponse<Article[]> | ErrorResponse;
// レスポンス処理関数
function handleApiResponse<T>(response: SuccessResponse<T> | ErrorResponse) {
if (response.success) {
return response.data; // TypeScriptが T型と推論
} else {
throw new Error(response.error.message);
}
}
集合操作の高度な概念
分布条件型(Distributive Conditional Types)
ユニオン型に対する条件型は、各構成要素に分配されます:
// Extract: ユニオン型から条件に合う型を抽出
type StringTypes = Extract<string | number | boolean, string>;
// 結果: string
// Exclude: ユニオン型から条件に合う型を除外
type NonStringTypes = Exclude<string | number | boolean, string>;
// 結果: number | boolean
複雑な型の組み合わせ例
// イベントの種類
type ClickEvent = { type: "click"; x: number; y: number };
type KeyEvent = { type: "keypress"; key: string };
type ScrollEvent = { type: "scroll"; scrollY: number };
type UIEvent = ClickEvent | KeyEvent | ScrollEvent;
// 特定のイベントタイプのみを抽出
type ClickEventOnly = Extract<UIEvent, { type: "click" }>;
// 結果: ClickEvent
// イベントハンドラーの型
type EventHandler<T extends UIEvent> = (event: T) => void;
// 使用例
const handleClick: EventHandler<ClickEvent> = (event) => {
console.log(`クリック位置: (${event.x}, ${event.y})`);
};
まとめ
TypeScript の型システムを集合の観点で理解することで:
-
ユニオン型(
|
):和集合 - 「A または B」の値を受け入れる -
インターセクション型(
&
):積集合 - 「A かつ B」の特性を持つ値
この概念を理解することで、複雑な型定義も直感的に理解できるようになります。
Discussion