🔗

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