Katsushi OUGI: Code & Life Notes – Life goes on

投稿日:

TypeScriptを理解するための集合論基礎

1.基本的な集合の概念

集合とは

集合とは、明確に定義された要素(オブジェクト)の集まり。TypeScriptでは、型は値の集合と考えることができる。

例:

  • number 型は、すべての数値(1,2,3.14,-5)の集合
  • "red" | "blue" | "green" 型は、3つの文字列値だけを含む集合

要素と集合の関係

数学では、要素Xが集合Aに含まれることを 「x ∈ A」 と表記する。含まれない場合は 「x ∉ A」
TypeScriptのコードで表現する。

type Colors = 'red' | 'blue' | 'green';

// "red" ∈ Colors("red"はColors型に含まれる)
const a: Colors = 'red'; // OK

// "yellow" ∉ Colors("yellow"はColors型に含まれない)
const b: Colors = 'yellow'; //エラー

空集合

空集合(∅)は要素を一つも持たない集合。TypeScriptでは never 型が該当する。never 型は、どんな値も割り当てることができない。

// 空集合に相当
type EmptySet = never;

// エラー: never型には何も代入できない
const empty: EmptySet = 'anything';

2.主要な集合演算

和集合(Union)

和集合 A ∪ B は、集合AまたはBのいずれか(あるいは両方)に属する要素からなる集合。
TypeScriptでは、ユニオン型 | が該当する。

type Numbers = 1 | 2 | 3;
type MoreNumbers = 3 | 4 | 5;

// Numbers ∪ MoreNumbers = 1 | 2 | 3 | 4 | 5
type AllNumbers = Numbers | MoreNumbers;

オブジェクトを例にすると以下のようになる。

interface Student {
  id: number;
  name: string;
  grade: number;
}

interface Teacher {
  id: number;
  name: string;
  subject: string;
}

// 学生または教師を表す型(和集合)
type SchoolPerson = Student | Teacher;

// 使用例
const person1: SchoolPerson = {
  id: 1,
  name: 'tanaka',
  grade: 3,
}; // OK(Student型)

const person2: SchoolPerson = {
  id: 2,
  name: 'yamada',
  subject: '数学',
}; // OK(Teacher型)

// 両方の特性を持つオブジェクトも有効
const person3: SchoolPerson = {
  id: 1,
  name: 'yamaguchi',
  grade: 3,
  subject: '数学',
}; // OK

// どちらの型も満たしていないのでエラーになる
const person4: SchoolPerson = {
  id: 1,
  name: 'yamaguchi',
}; // Error

ユニオン型の変数に対しては、両方の型に共通して存在するプロパティにしか直接アクセスができない。 特定のプロパティにアクセスしたい場合は型ガードの処理が必要になる(ininstanceof)。

if ("grade" in person) {
  ...
}

積集合(Intersection)

積集合 A ∩ B は、集合Aと集合B両方に属する要素からなる集合。TypeScriptでは、インターセクション型 & がこれに該当する。

interface Person {
  name: string;
  age: number;
}

interface Employee {
  company: string;
  age: number;
}

// Person ∩ Employee
type PersonEmployee = Person & Employee;

// 例
const person: PersonEmployee = {
  name: 'yamada',
  age: 22,
  company: 'Panasonic',
}; // OK

const person2: PersonEmployee = {
  age: 22,
}; // NG

オブジェクト型のインターセクションは、両方のプロパティを合わせた型になる。和集合と比較してみた。

type PersonEmployee = Person & Employee;

// nameが無いからエラーになる
const person2: PersonEmployee = {
  age: 22,
  company: 'yamada',
};

type PersonOrEmployee = Person | Employee;
// Employee型に慨するのでOK
const person3: PersonOrEmployee = {
  age: 11,
  company: 'yamada',
};

疑問が残る。

const person2: PersonEmployee = {
  age: 22,
}; // NG

これがなぜNGなのか。 集合論においては「積集合 A ∩ B は、集合Aと集合B両方に属する要素からなる集合」という定義は正しい。 しかし、TypeScriptのオブジェクト型においては、この概念が特殊な形で表現される。

  1. オブジェクト型は「特定のプロパティを持つオブジェクト」の集合を表す。
  2. あるオブジェクトがA型に属するとは「A型が要求するプロパティをすべて持っている」ことを意味する。

そのため、A型とB型の両方に属するオブジェクト(積集合の要素)は「A型の要求するすべてのプロパティ」と「B型の要求するすべてのプロパティ」の両方を持つオブジェクトになる。

「積集合 A ∩ B は、集合Aと集合B両方に属する要素からなる集合」これを文面どおりに直感的な理解のままに表現したい場合は以下のようになる。

type PersonEmployeeFields = keyof (Person | Employee); // type PersonEmployeeFields = "age"
  1. Person | Employee はユニオン型(和集合)で、「PersonまたはEmployee型」のオブジェクトを表す。
  2. keyof 演算子をユニオン型に適用すると、「両方の型に共通して存在するプロパティ名のみ」が抽出される。

Person | Employee は和集合(ユニオン)なのに、keyof を適用すると積集合(インターセクション)的な振る舞いをするのはなぜなんだ?

  • Person | Employee 型の値はPerson型かEmployee型のどちらかです。
  • その値に安全にアクセスできるプロパティは、どちらの型でも存在することが保証されているプロパティだけです。安全に、とは型ガードを利用せずともアクセスできるプロパティのこと。
  • その「どちらの型でも存在するプロパティ」が keyof (Person | Employee) で得られる結果となる。

これが「プロパティのインターセクションに関する直感は、2つのインターフェースのインターセクションではなく、ユニオンに当てはまる」と言われる理由。

上記の例のようにオブジェクト型のインターセクションは、両方のプロパティを合わせた型になるが、プリミティブ型の場合は異なる。

type StringsAndNumbers = string & number;
// never(空集合 - stringでありnumberである値は存在しない)

差集合

差集合 A \ B は、集合Aに属し、集合Bに属さない要素からなる集合。 TypeScriptには直接的な演算子はないが、条件型などで表現できる。

type AllColors = 'red' | 'blue' | 'green' | 'yellow';
type PrimaryColors = 'red' | 'blue' | 'yellow';

// AllColors - PrimaryColors = "green"
type NonPrimaryColors = Exclude<AllColors, PrimaryColors>;

3.集合の関係

部分集合

A ⊆ B は、「集合Aのすべての要素が集合Bにも属する」ことを意味する。 TypeScriptでは、extends キーワードが対応する。

type Animals = 'dog' | 'cat' | 'bird';
type Pets = 'dog' | 'cat';

// Pets ⊆ Animals(すべてのペットは動物である)
type isPetSubsetOfAnimal = Pets extends Animals ? true : false;
// 結果 true

真部分集合

A ⊂ B は、「AはBの部分集合であり、かつA≠B」を意味する。 TypeScriptで直接表現する方法はないが、概念として理解しておくと便利。

4.特別な集合の性質

互いに素な集合

互いに素な集合は、共通要素を持たない集合。A ∩ B = ∅ TypeScriptでは、矛盾する制約を持つ型が never になる現象として現れる。

type NumbersOnly = number;
type StringsOnly = string;

// NumbersOnly ∩ StringsOnly = ∅(空集合)
type Impossible = NumbersOnly & StringsOnly; // never

TypeScriptの実践例

TypeScriptの型システムで集合論がどのように応用されているかの例。

// キーの取得('name')
type Person = { name: string; age: number };
type PersonKeys = keyof Person; // "name" | "age"

// 複数のオブジェクト型のユニオンに対するkeyof
interface A {
  a: number;
  common: string;
}
interface B {
  b: number;
  common: string;
}
type AB = A | B;
type KeyofAB = keyof AB; // "common"(両方の型に共通するキーのみ)

// 条件型を使った部分集合の表現
type IsSubset<T, U> = T extends U ? true : false;
type CheckSubset = IsSubset<'a' | 'b', 'a' | 'b' | 'c'>; // true