型のメンタルモデル
型システムの背景理論
プログラミング言語の型システムにはそれぞれ固有の世界観があり、言語ごとに型の機能が異なります。
その一方で複数の言語で共通している機能もあり、それらのさまざまな型の機能は唐突にどこからともなく出現してきたわけではありません。背景として大きくは型理論(type theory)と呼ばれる数学的な研究分野があり、各言語の型システムは型理論に基づいて実装されています。
たとえば、TypeScriptのunknown
型やnever
型のような一見何のためにあるか分からないような型であっても、型理論においてはその役割や機能を一般的に説明することができます。これらの型はトップ型やボトム型と呼ばれる型の種類に分類され、部分型関係が作る階層構造の両端点に位置する型として振る舞います。
型理論的な観点からの知識を持つことで似たような型システムを持つ他の言語においても型の機能について自然に推論することが可能になります。たとえばScalaというプログラミング言語ではNothing
と呼ばれる型が型階層のボトムに位置することからnever
型と同じ働きをすることが推論できます。このように型について一般化された知識を使うことで、プログラミング言語をスイッチするような場合でもスムーズに機能の類推や学習を行うことができるようになります。
型理論は非常に奥深く難解な分野でもありますが、その一方で比較的簡単に理解できて実用的にも役立つ概念も非常に多くあります。このドキュメントではそういった知識からTypeScriptの型の世界観、いわばメンタルモデルを構築するための知識の一部を紹介します。
この章の内容を読んでみて型システムや型理論について興味が湧いたら、著名な入門書である『型システム入門 プログラミング言語と型の理論』の単純型や部分型付けの章などを読んでみることをオススメします。
論理学的な知識があると推論規則などが比較的読みやすくなるので、東京大学出版会から出版されている『論理学』などの書籍を合わせて読むとよいかもしれません。また、型システム入門の読み方としては定理を隅々検証して読むというよりかは、知識や概念を入手する目的で面白そうなところを拾って読んでいくと意外と読みやすくなるのでぜひ挑戦してみてください。
集合論的なデザイン
型のメンタルモデル、つまり「型をどのように解釈するか」を考える上で非常に有用でありなが身近な数学的なツールがあります。それが集合論(set thoery)であり、この章では「型=集合」として考えることにします。
一般に型(type)は集合(set)は異なる概念ですが、型理論と集合論の間には密接な関連があります。
特にTypeScriptにおいては、型を集合論的に扱えるようなデザインが意図的になされており、型を「値の集合」として捉えることで直感的に型を理解することができるようになっています。この見方は決して偏ったものではなく、公式ドキュメントでも推奨されている型の考え方です。
本章ではこのような集合論的な見方に立って型を考えることで、型の振る舞いについての自然な推論を行えるようなメンタルモデルを構築します。
和集合と共通部分
型を集合論的に扱えるお陰で、TypeScriptの型は集合が持つような演算の一部を利用することができます。
集合の演算は集合から新しい集合を作り出すような操作であり、そのような演算にはいくつも種類があります。TypeScriptではそのような演算の中で和集合と共通部分を演算に相当するユニオン型とインターセクション型が備わっています。
ts
typeA = {a : string };typeB = {b : number };// AとBの和集合を表現する型typeUnion =A |B ;// AとBの共通部分を表現する型typeIntersection =A &B ;
ts
typeA = {a : string };typeB = {b : number };// AとBの和集合を表現する型typeUnion =A |B ;// AとBの共通部分を表現する型typeIntersection =A &B ;
直感的にはユニオン型はふたつの集合の和集合を表現する型であり、インターセクション型はふたつの型の共通部分を表現する型です。ユニオン型は特に型の絞り込み(narrowing)において特に重要な役割を果たし、型の和集合から選択的に型の候補を削っていくことができます。
型の絞り込みは和集合から集合を削っていくts
typeStrOrNum = string | number;functionnarrowUnion (param :StrOrNum ) {if (typeofparam === "string") {// stringとnumberの和集合からstringを削るconsole .log (param .toUpperCase ());} else {// 残された集合はnumberconsole .log (param * 3);}}
型の絞り込みは和集合から集合を削っていくts
typeStrOrNum = string | number;functionnarrowUnion (param :StrOrNum ) {if (typeofparam === "string") {// stringとnumberの和集合からstringを削るconsole .log (param .toUpperCase ());} else {// 残された集合はnumberconsole .log (param * 3);}}
このふたつの型は複数の型から新しい型を合成できるという点で演算として重要ですが、特定の型そのものが集合としてどのように解釈できるかを次に紹介する3つの型で解説していきます。
ユニット型
ここからは、TypeScriptにおいて型は値の集合として扱えることができることを具体例を交えて説明していきます。
まずは単に型を「値の集合」であると考えてください。たとえば、number
型という数値を表す型ですが、この型が集合であるとすると、その要素は具体的なnumber
型の値である数値です。たとえば1
や3.14
などの数値がこの集合の要素となります。number型のページで述べているようにnumber型で表現可能な範囲は有限であり、それらの範囲の要素にNaN
とInfinity
などの特殊な定数を加えた集合がnumber
型の集合ということになります。
さて、重要な型の概念としてユニット型(unit type)という型の種類があります。ユニット型とは文字通りの単位的な型であり、型の要素として値をひとつしか持たないような型です。集合論においては単一の要素からなる集合は単位集合(unit set)や単集合(singleton set)など呼ばれます。
型の世界での単位集合に相当するものがユニット型であり、たとえば、PHPではnull
という単一の値を持つnull
型がユニット型に相当し、KotlinやScalaでは分かりやすくUnit
型という名前の型がユニット型です。
TypeScriptではnull
という単一の値を持つnull
型と、undefined
という単一の値を持つundefined
型がユニット型に相当します。
nullとundefinedはユニット型ts
typeN = null;constn :N = null;typeU = undefined;constu :U =undefined ;
nullとundefinedはユニット型ts
typeN = null;constn :N = null;typeU = undefined;constu :U =undefined ;
他にもTypeScriptにはリテラル型という型がありましたが、このリテラル型もユニット型に相当します。
リテラル型はユニット型ts
typeUnit = 1;constone :Unit = 1;
リテラル型はユニット型ts
typeUnit = 1;constone :Unit = 1;
リテラル型は値リテラルをそのまま型として表現できる型であり、number
やstring
などのプリミティブ型にはそれぞれ具体的な値のリテラルによって作成されるリテラル型が存在します。
- 文字列リテラル型 :
"st"
,"@"
, ... - 数値リテラル型 :
1
,3.14
,-2
, ... - 真偽値リテラル型 :
ture
,false
のふたつのみ
型は値の集合でしたが、具体的な値はそのリテラル型と一対一で対応します。
集合の要素の個数は「濃度(cardinality)」と呼ばれる概念によって一般化され、基数という数によって表記されます。たとえば、要素がひとつしかない単位集合の濃度は1です。つまり、型を集合としてみなしたときのユニット型の濃度は1ということになります。
それでは濃度が2、つまり要素の個数が二個からなるシンプルな型について考えてみましょう。たとえば、真偽値を表す boolean
という型の要素(値)はtrue
とfalse
のみであり、boolean
型の変数にはそれら以外の値を割り当てることはできません。したがってboolean
型は濃度2の集合としてみなせます。
ts
constb1 : boolean = true;constb2 : boolean = false;constType 'number' is not assignable to type 'boolean'.2322Type 'number' is not assignable to type 'boolean'.: boolean = 1; b3
ts
constb1 : boolean = true;constb2 : boolean = false;constType 'number' is not assignable to type 'boolean'.2322Type 'number' is not assignable to type 'boolean'.: boolean = 1; b3
リテラル型について思い出すと真偽値についてもそれぞれリテラル型true
とfalse
が存在しました。これらの型はそれぞれがひとつの値だけを持つユニット型でした。
リテラル型は具体的な値と一対一の対応となります。型の集まりには集合演算が備わっていたので、リテラル型を要素として新しい集合を作ってみると考えてもよいでしょう。ふたつの単集合true
とfalse
を合成してふたつの型(あるいは値)から和集合を作成すると濃度2の型を得ることができます。
true と false の和集合ts
typeBool = true | false;
true と false の和集合ts
typeBool = true | false;
このようにユニオン型で合成した型Bool
はboolean
型と同一の型となります。
ボトム型
ユニット型は値をひとつしか持たない型ですが、値をまったく持たないような型も存在しています。そのような型をボトム型(bottom type)と呼びます。型が集合であるとするとき、ボトム型は空集合(empty set)に相当し、空型(empty type)とも呼ばれることがあります。
ボトム型は値をまったく持たない型として、例外が発生する関数の返り値の型として利用されますが、TypeScriptでのボトム型は部分型階層の一番下、つまりボトムの位置に存在しているnever
型となります。
ts
functionneverReturn (): never {throw newError ("決して返ってこない関数");}
ts
functionneverReturn (): never {throw newError ("決して返ってこない関数");}
never
型は集合としては空集合であり、値をひとつも持たないため、その型の変数にはどのような要素も割り当てることができません。
ts
constType 'number' is not assignable to type 'never'.2322Type 'number' is not assignable to type 'never'.: never = 42; n
ts
constType 'number' is not assignable to type 'never'.2322Type 'number' is not assignable to type 'never'.: never = 42; n
トップ型
ボトム型が値をまったく持たない型なら、それとは逆にすべての値を持つような型も存在しています。そのような型をトップ型(top type)と呼びます。
トップ型はすべての値を持っており、その型の変数にはあらゆる値を割り当てることができます。オブジェクト指向言語であれば大抵は型階層のルート位置、つまりトップ位置に存在している型であり、TypeScriptではunknown
型がトップ型に相当します。
ts
constu1 : unknown = 42;constu2 : unknown = "st";constu3 : unknown = {p : 1 };constu4 : unknown = null;constu5 : unknown = () => 2;
ts
constu1 : unknown = 42;constu2 : unknown = "st";constu3 : unknown = {p : 1 };constu4 : unknown = null;constu5 : unknown = () => 2;
ボトム型が空集合に相当するなら、トップ型は全体集合に相当すると言えるでしょう。なおTypeScriptでは{} | null | undefind
という特殊なユニオン型をunknown
型相当として扱い、相互に割当可能としています。
unknown型相当の特殊なユニオン型ts
declare constu : unknown;constt : {} | null | undefined =u ;
unknown型相当の特殊なユニオン型ts
declare constu : unknown;constt : {} | null | undefined =u ;
{}
はプロパティを持たないオブジェクトを表現する空のobject型であり、この型はあらゆるオブジェクトの型とnull
とundefined
を除くすべてのプリミティブ型を包含しています。したがって、unknown
という全体集合は上記のような3つの集合に分割できると考えることもできます。
TypeScriptにはunknwon
型以外にもうひとつ特殊なトップ型があります。それがany
型です。any
型にはunknown
型と同様にあらゆる型の値を割当可能です。
ts
consta1 : any = 42;consta2 : any = "st";consta3 : any = {p : 1 };consta4 : any = null;consta5 : any = () => 2;
ts
consta1 : any = 42;consta2 : any = "st";consta3 : any = {p : 1 };consta4 : any = null;consta5 : any = () => 2;
any
型の特殊性はトップ型としてあらゆる型からの割当が可能だけでなく、never
型を除くあらゆる型へも割当可能な点です。
ts
declare consta : any;constn1 : unknown =a ;constn2 : {} =a ;constn3 : number =a ;constn4 : 1 =a ;constType 'any' is not assignable to type 'never'.2322Type 'any' is not assignable to type 'never'.: never = n5 a ;
ts
declare consta : any;constn1 : unknown =a ;constn2 : {} =a ;constn3 : number =a ;constn4 : 1 =a ;constType 'any' is not assignable to type 'never'.2322Type 'any' is not assignable to type 'never'.: never = n5 a ;
any
型はnever
型を除けばあらゆる型へも割当可能なため一見するとボトム型のように振る舞っているように見えますが、実際にはボトム型ではありません。
TypeScriptは元来、JavaScriptに対してオプショナルに型付けを行うという言語であり、型注釈を省略して型推論ができない場合には未知の型を暗黙的にany
型として推論します。このような状況においてany
型はあらゆる型からの割当が可能であるだけでなく、あらゆる型への割当が可能であることが必要であり、それによって型注釈がないJavaScriptに対して漸進的に型を付けていくことが可能になります。
実はany
型はunknown
型がTypeScriptに導入されるまで唯一のトップ型として機能していましたが、純粋にあらゆる型の上位型になる部分型関係のトップ位置の型として機能するunknown
型が導入されたことで部分型関係の概念が明瞭になりました。
部分型関係の解釈
部分型関係とはそもそも「型Bが型Aの部分型であるとき、Aの型の値が求められる際にBの型の値を指定できる」という型同士の互換性に関わる関係です。関数型を除く通常の型については、ここまで見てきた通り型を集合として解釈すれば部分型関係は集合の包含関係に相当します。
部分型関係の説明において、型と型の関係性は階層構造で捉えることができると述べましたが、集合の包含関係は階層構造について少し見方を変えた構造であると言えます。
トップ型であるunknown
型はあらゆる型の基本型、つまり上位型として振る舞い、あらゆる型はunknown
型の部分型となります。したがって、型を集合として解釈したとき、unknown
型はTypeScriptにおけるあらゆる値を含む集合となります。つまり全体集合であり、あらゆる型はunknown
型の部分集合とみなすことができます。
それとは逆に、ボトム型であるnever
型はあらゆる型の部分型となります。したがって、型を集合として解釈したとき、never
型はTypeScriptにおけるどのような値も含まない集合、つまり空集合としてみなすことができます。
このように部分型関係を集合の包含関係として捉えることで、より直感的に型の互換性についての推論が可能となります。
たとえばふたつの集合の和集合はその共通部分を包含します。ユニオン型とインターセクション型は和集合と共通部分に相当していたので、包含関係からインターセクション型がユニオン型の部分型となることが推論されます。実際に検証してみると、ユニオン型の変数にインターセクション型の変数を割りあてることが可能です。
ts
typeA = {a : string };typeB = {b : number };typeUnion =A |B ;typeIntersection =A &B ;consta_and_b :Intersection = {a : "st",b : 42 };consta_or_b :Union =a_and_b ;
ts
typeA = {a : string };typeB = {b : number };typeUnion =A |B ;typeIntersection =A &B ;consta_and_b :Intersection = {a : "st",b : 42 };consta_or_b :Union =a_and_b ;