はじめに
普段 Java を書いているエンジニアです。最近フロントエンド開発もやるようになり、JavaScript の勉強をして Qiita に記事投稿していましたが、ついに TypeScript にも手を出しました。ユニオン型とインターセクション型を学んだときに混乱したので、私が悩んだポイントを整理して、同じように混乱している人の助けになればと思い記事にします。
表記について
Union Type、Intersection Type の訳語は人によって「共用体型」や「合併型」、「交差型」だったりしますが、本記事では次のカタカナ表記で統一します。
- ユニオン型 (Union Type)
- インターセクション型 (Intersection Type)
疑問
インターセクション型を学んだとき、次のような疑問を持ちました。
「インターセクション型は、意味的にはユニオン型と呼ぶべきではないのか?」
どういうことか詳しく説明します。
集合とは
その前に、TypeScript のユニオン型、インターセクション型という名前は、おそらく数学の和集合(Union)と積集合(Intersection)から来ていると思うので、軽く数学のおさらいをします。
和集合(Union, ユニオン)
2つの集合の要素を合わせた集合。
例
| 集合 | 要素 | |
|---|---|---|
| A | 1, 2, 3, ... | 自然数 |
| B | ..., -6, -3, 0, 3, 6, ... | 3の倍数 |
| A と B の和集合 | ..., -6, -3, 0, 1, 2, 3, 4, 5, 6, ... |
積集合(Intersection, インターセクション)
2つの集合の共通部分。
例
| 集合 | 要素 | |
|---|---|---|
| A | 1, 2, 3, ... | 自然数 |
| B | ..., -6, -3, 0, 3, 6, ... | 3の倍数 |
| A と B の積集合 | 3, 6, ... |
インターセクション型はユニオン型では?
次の例では A と B のインターセクション型を引数に取る関数を定義しています。
この関数の引数 arg は A のプロパティと B のプロパティどちらも持っていることが保証されるので、コンパイル可能です。
type A = {
name: string;
}
type B = {
age: number;
}
function someFunc(arg: A & B) {
console.log(`name = ${arg.name}`);
console.log(`age = ${arg.age}`);
}
持っているプロパティを整理すると次のようになります。
2つの型のプロパティを全部持った型になる、ということは、和集合を作っている。
ん・・・?和集合(ユニオン)なのにインターセクション型っていう名前なの・・・?
と考えてよくわからなくなってしまいました。
| 型 | プロパティ |
|---|---|
| A | name: string |
| B | age: number |
| A & B | name: string, age: number |
勘違いしていたポイント
TypeScript の型は「プロパティの集合」と思っていた。
代入可能な値の集合
実際にはインターセクション型で扱う集合は「プロパティの集合」ではなく「代入可能な値の集合」です。
さっきと同じ A と B という型を使います。
type A = {
name: string;
}
type B = {
age: number;
}
このとき、A 型の変数、B 型の変数に代入できるものはどういうオブジェクトになるでしょうか?
A 型に代入可能なのは {name: 'Alice'}、{name: 'Bob'}、{name: 'Charlie'} のような値。
さらに、余分なプロパティを持っていても問題ないので、{name: 'Dave', age: 99}、{name: 'Eiji', id: 1000} のような値も代入可能です。
B 型に代入可能なのは {age: 20}、{age: 33}、{age: -100} のような値。
さらに、余分なプロパティを持っていても問題ないので、{name: 'Dave', age: 99}、{age: 123, title: 'hoge'} のような値も代入可能です。
ここで、A & B 型に代入可能な値は 「Aに代入可能な値の集合」と「Bに代入可能な値の集合」の積集合(インターセクション)になります。上で挙げた例であれば {name: 'Dave', age: 99} がそうですね。
ベン図で書くと次のようになります。
次に A & B 型が持っているプロパティを確認してみましょう。
A 型に代入できて、B 型にも代入できる。ということは A 型のプロパティも B 型のプロパティも両方持っています。
つまり、インターセクション型の変数が持っているプロパティは2つの型のプロパティの和集合になります。
まとめ
- インターセクション型で作成されるのは、それぞれの型に代入可能な値の 積集合(インターセクション)
- インターセクション型の持つプロパティは、それぞれの型に持っているプロパティの 和集合(ユニオン)
あくまでも型としては積集合で、持っているプロパティが 結果的に 和集合になっているだけだったのです。
ということで、インターセクション型は名前通りの型なのでした。
おまけ
プリミティブ型やリテラル型のインターセクション型を作ってみると理解が深まるかもしれません。あくまでも「代入できる値」の積集合を作るので、次の例では X は never 型、 Y は 'hoge' 型、Z は 999 型になります。
type X = number & string; // => never
type Y = 'hoge' & string; // => 'hoge'
type Z = 999 & number; // => 999
と思ったら any が入ってくると不思議な挙動になることを発見しました。集合として考えたら unknown & string も any & string も両方 string になると思ったんですけどね… any は「なんでも代入できる」だけではなく「型チェックを一切放棄する」ので結果は any になるんでしょうか?
TypeScript 難しい…
type UnkownString = unknown & string; // => string
type AnyString = any & string; // => any