Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

TypeScriptのUnion / Intersection Typesで遊んだ

(筆者は型入門者ですので,いい加減な事を書いていたらご指摘ください)

  • ★追記: intersection typeとunion typeが逆だったので直した。
  • ★追記2: susisuくんにいいこと教えてもらったので追記した。

Intersection Types

TypeScriptの型,とくにIntersection Typesが面白くて,T & Uという記法を使うと,TUの両方を満たす型を生成できる。動的に型を生成できるので,まさに型計算という感じで面白い。 例えばabというフィールドを持つ型のTと,bcというフィールドを持つ型の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 };

もちろん,TUとが矛盾するような場合は型を作ることができない。実際は作れるのだが,その部分の型は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として扱われている。

型とは,値の集合である,というふうに考えるとわかりやすい,というか実際そうである。全ての真偽値(truefalseしかない)の集合がbooleanであり,全ての整数の集合がintegerである。もっとも,無限集合である一方でコンピュータは有限桁しか扱えないので,それを基準にした有限集合をintegerとしている。

ちなみに,型は値の集合である,というところからいろいろなところが見えてくる。継承関係は集合の包含関係で考えられるし,関数は集合から集合への射(全射かも単射かもしれないし,その両方かもしれない)と考えることができるし,さきほどのT & Uの例にしても,TUとの共通集合\(T \cap U\)を求めたというふうに考えられる。このため&記号を使っているのだと思う。そして,TUとが両方持っているbについて,共通集合を求めようとしたところ,numberbooleanとでは共通の要素がないから,空になってしまったのだ。

ここで、様々な型について図で考えてみると分かりやすい。

f:id:Windymelt:20211125222516p:plain
trueしか値をとらない型Trueと、falseしか値をとらない型Falseがあったとき、True & Falseは値をとらないし、True | Falseはbooleanそのものになる

f:id:Windymelt:20211125220652p:plain
2つのオブジェクトについて考えると、双方のフィールドやメソッドが存在していれば、intersection typeの制約を満たせる

共通の要素がある場合について考える。以下の例では,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 & Ubの型はBになる。

Union Types

TもしくはUという型も考えることができる。TypeScriptではT | Uという表記を使う。先程登場した"x" | "y" | "z"も,xかyかzのどれかを取るという意味である。

これも,型を値の集合として捉えると簡単で,\(T \cup U\)となるような型を生成している。

まあこれはScalaでおなじみのEitherなので,はあそうですねという感じだった。任意個をくっつけられるEither

おおまかに図にすると以下のようになる。

f:id:Windymelt:20211125223009p:plain
型が広くなるほどとりうる値の数は増える。型が狭くなるほどとりうる値の数は減る。

T | U とすると「広くなる」イメージから、オブジェクトの場合はフィールドが増えそうな直感が働いてしまうが、これは誤り。型の制約が緩くなり、Tにあるフィールドか、Uにあるフィールドのどちらか、もしくは両方がある事までしか型が保証しないので、TUにしかないフィールドは使えない。

T & U とすると「狭くなる」イメージから、オブジェクトの場合はフィールドが減りそうな直感が働いてしまうが、これは誤り。型の制約がきつくなり、Tにあるフィールドも、Uにあるフィールドもあることを型が保証するので、Tにあるフィールドも、Uにあるフィールドも使うことができる。

共通部分だけ残す演算?

ところで,こういう型を考えることはできないだろうか。TUとを受取り,その両方だけにあるフィールドを持つ型V: { b }を生成するものである。Ta bを持っているし,Ub 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 }は,一見TUの部分に見えるが,TUも,b以外のフィールドを持つことではじめてTUになるのであって,実はVとのかかわりあいがない。言い換えると,Vは別段TUの部分型ではなさそう。というのもTUがとりうる値の集合に,bしかフィールドを持たない型は登場しないからだ。もしabが省略可能なら作ることもできる。

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にせずともこういう操作ができないだろうか。需要があるのか知らないが。VTUの部分型ではない(多分。識者の知見をお待ちしています)ので,あまり簡単ではなさそう。

追記

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] };
★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?