(筆者は型入門者ですので,いい加減な事を書いていたらご指摘ください)
- ★追記: intersection typeとunion typeが逆だったので直した。
- ★追記2: susisuくんにいいこと教えてもらったので追記した。
Intersection Types
TypeScriptの型,とくにIntersection Typesが面白くて,T & Uという記法を使うと,TとUの両方を満たす型を生成できる。動的に型を生成できるので,まさに型計算という感じで面白い。
例えばaとbというフィールドを持つ型のTと,bとcというフィールドを持つ型のUを使って,a b cというフィールドを持つT & U型ができる,という具合。
Scalaよりもこういうところに力を入れているっぽい。
type A = any; type B = any; type C = any; type T = { a: A, b: B }; type U = { b: B, c: C }; type TUI = T & U; // => { a: any, b: any, c: any } const tui: TUI = { a: true, b: true, c: true };
もちろん,TとUとが矛盾するような場合は型を作ることができない。実際は作れるのだが,その部分の型はneverになるので,値を入れることができない。
type A = any; type B = boolean; type B2 = number; type C = any; type T = { a: A, b: B }; type U = { b: B2, c: C }; type TUI = T & U; // => { a: any, b: never, c: any } const tui: TUI = { a: true, b: true, c: true }; // doesn't compile!
値の集合としての型
これは面白くて,取れる値の共通集合を探そうとして,結局空になってしまったということを意味している。TypeScriptでは,取れる値のない型,つまりその型の値の集合が空集合(\(\emptyset\))になるような型はneverとして扱われている。
型とは,値の集合である,というふうに考えるとわかりやすい,というか実際そうである。全ての真偽値(trueかfalseしかない)の集合がbooleanであり,全ての整数の集合がintegerである。もっとも,無限集合である一方でコンピュータは有限桁しか扱えないので,それを基準にした有限集合をintegerとしている。
ちなみに,型は値の集合である,というところからいろいろなところが見えてくる。継承関係は集合の包含関係で考えられるし,関数は集合から集合への射(全射かも単射かもしれないし,その両方かもしれない)と考えることができるし,さきほどのT & Uの例にしても,TとUとの共通集合\(T \cap U\)を求めたというふうに考えられる。このため&記号を使っているのだと思う。そして,TとUとが両方持っているbについて,共通集合を求めようとしたところ,numberとbooleanとでは共通の要素がないから,空になってしまったのだ。
ここで、様々な型について図で考えてみると分かりやすい。


共通の要素がある場合について考える。以下の例では,Bは"x"か"y"か"z"のどれかを取る型であり,B2は単なるstringである。|については次節で見ていく。
type A = any; type B = "x" | "y" | "z"; type B2 = string; type C = any; type T = { a: A, b: B }; type U = { b: B2, c: C }; type TUI = T & U; type TUU = T | U; const tui: TUI = { a: true, b: "x", c: true };
すると,取りうる値のことを考えると,\(B \subset B2\)であるから,T & Uのbの型はBになる。
Union Types
TもしくはUという型も考えることができる。TypeScriptではT | Uという表記を使う。先程登場した"x" | "y" | "z"も,xかyかzのどれかを取るという意味である。
これも,型を値の集合として捉えると簡単で,\(T \cup U\)となるような型を生成している。
まあこれはScalaでおなじみのEitherなので,はあそうですねという感じだった。任意個をくっつけられるEither。
図
おおまかに図にすると以下のようになる。

T | U とすると「広くなる」イメージから、オブジェクトの場合はフィールドが増えそうな直感が働いてしまうが、これは誤り。型の制約が緩くなり、Tにあるフィールドか、Uにあるフィールドのどちらか、もしくは両方がある事までしか型が保証しないので、TかUにしかないフィールドは使えない。
T & U とすると「狭くなる」イメージから、オブジェクトの場合はフィールドが減りそうな直感が働いてしまうが、これは誤り。型の制約がきつくなり、Tにあるフィールドも、Uにあるフィールドもあることを型が保証するので、Tにあるフィールドも、Uにあるフィールドも使うことができる。
共通部分だけ残す演算?
ところで,こういう型を考えることはできないだろうか。TとUとを受取り,その両方だけにあるフィールドを持つ型V: { b }を生成するものである。Tはa bを持っているし,Uはb cを持っている。ここからbだけを持つ型を作れないだろうか。
type A = any; type B = any; type C = any; type T = { a: A, b: B }; type U = { b: B, c: C }; type XXX = ???; const something: XXX = { b: true };
ここでさっきの集合の話が登場する。bだけを持つ型V: { b }は,一見TやUの部分に見えるが,TもUも,b以外のフィールドを持つことではじめてTやUになるのであって,実はVとのかかわりあいがない。言い換えると,Vは別段TやUの部分型ではなさそう。というのもTやUがとりうる値の集合に,bしかフィールドを持たない型は登場しないからだ。もしaやbが省略可能なら作ることもできる。
type A = any; type B = any; type C = any; type T = { a?: A, b: B }; type U = { b: B, c?: C }; type XXX = T & U; type XXX2 = T | U; // こちらでも通る const something: XXX = { b: true };
しかし元の型をnullableにせずともこういう操作ができないだろうか。需要があるのか知らないが。VはTやUの部分型ではない(多分。識者の知見をお待ちしています)ので,あまり簡単ではなさそう。
追記
id:susisuがいいこと教えてくれました。
// T と U は共通のフィールド b: number を持つ type T = { a: string, b: number }; type U = { b: number, c: boolean }; // ここから T と U の共通部分だけを持つ型 { b: number } が欲しい // T と U の union は T と U の supertype だが, a または c が必須となってしまう type X = T | U; // ここで keyof X とすると union の共通のキー b のみが抜き出される type KX = keyof X; // これと mapped type を使ってやると欲しかったものが作れる type Y = { [K in keyof X]: X[K] };