(筆者は型入門者ですので,いい加減な事を書いていたらご指摘ください)
- ★追記: 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] };