型のメンタルモデル
型システムの背景理論
プログラミング言語の型システムにはそれぞれ固有の世界観があり、言語ごとにできること (型の機能) が異なります。
その一方で、共有している機能もあるわけで、それらのさまざまな型の機能は唐突にどこからともなく出現してきたわけではありません。背景として大きくは型理論 (Type theory) と呼ばれる研究分野があり、各言語の型システムは型理論に基づいて実装されています。
たとえば、TypeScript の unkown
型や never
型のような一見何のためにある型かわからないようなものであっても、型理論においてはその役割や機能を一般的に説明することができます。これらの型は Top 型や Bottom 型と呼ばれる型の種類に分類され、部分型関係の端点に位置する型として振る舞います。
さらに型理論的な観点からの一般知識を持つことで似たような型システムを持つ他の言語においても型の機能について自然に推論することが可能になります。たとえば Scala という言語では Any
型と Nothing
型が unknown
型と never
型と同じ働きをすることが推論できます。一般化された型についての知識を使えるため、プログラミング言語をスイッチするような場合においてもスムーズに機能の類推や学習を行うことができるようになります。
ただし、型理論は非常に奥深く難解でもあるため、一般にはとっつきにくい分野です。その一方で、比較的簡単に理解できて実用的にも役立つ概念も非常に多くあります。このドキュメントではそういった知識から TypeScript の型の世界観、いわばメンタルモデルを構築するための知識を紹介します。
集合論的なデザイン
一般に型(type)は集合(set)は異なる概念ですが、型理論と集合論には密接な関連があります。
特に TypeScript においては、型を集合論的に扱えるようなデザインが意図的になされており、型を「値の集合」として捉えることで直感的に型を理解することができるようになっています。この見方は決して偏ったものではなく、公式ドキュメントでも推奨されている型の考え方です。
本章では、このような集合論的な見方に立って型を考えることで、型の振る舞いについての自然な推論を行えるようなメンタルモデルを構築します。
集合演算
型を集合論的に扱えるお陰で、TypeScript の型は集合が持つような演算の一部を利用することができます。
集合の演算は集合から新しい集合を作り出すような操作であり、そのような演算にはいくつも種類がありますが、TypeScript では和集合と共通部分を作り出すことができる演算が備わっています。
ユニオン型とインターセクション型はまさに和集合と共通部分を作る演算に相当します。
型の和集合と共通部分ts
typeA = {fst : string };typeB = {snd : number };typeUnion =A |B ;typeIntersection =A &B ;
型の和集合と共通部分ts
typeA = {fst : string };typeB = {snd : number };typeUnion =A |B ;typeIntersection =A &B ;
直感的にはユニオン型はふたつの集合の和を表現する型であり、インターセクション型はふたつの型の共通部分を表現する型です。ユニオン型とインターセクション型をこのような集合論的な見方で扱うには次に紹介する3つの型が重要となります。
ユニット型
ここからは、TypeScript において型は値の集合として扱えることができることを具体例を交えて説明していきます。
まずは単に型は値の集合であると考えてください。たとえば、number
型という数値を表す型ですが、この型が集合であるとするとその要素は具体的な number
型の値である数値です。たとえば 1
や 3.14
などの数値がこの集合の要素となります。次の章で述べたように number
型で表現可能な範囲は有限であり、それらの範囲の要素に NaN
と Infinity
などの特殊な定数を加えた集合が number
型の集合ということになります。
📄️ number型
JavaScriptのnumber型は、1や-1などの整数と0.1などの小数を含めた数値の型です。PHPなどの言語では、数値について整数を表す型(int)と小数を表す型(floatやdouble)の2つの型を持ちます。Javaなどの言語では、整数型をさらに32ビットと64ビットに細分化する言語もあります。JavaScriptには、整数と小数を型レベルで区別するものはありません。どちらもnumber型で表現します。
さて、重要な型の概念として、型理論ではユニット型(Unit type)と呼ばれる型があります。ユニット型とはそのまま単位的(unit)な型であり、値をひとつしか持たないような型です。思い出してほしいのは、TypeScriptにはリテラル型という型がありました。TypeScript におけるユニット型はこのリテラル型などが相当します。
リテラル型はユニット型ts
typeUnit = 1;constone :Unit = 1;
リテラル型はユニット型ts
typeUnit = 1;constone :Unit = 1;
リテラル型は値リテラルをそのまま型として表現できる型であり、number
や string
などのプリミティブ型にはそれぞれ具体的な値のリテラルによって作成されるリテラル型が存在します。
- 文字列リテラル型 :
"st"
,"@"
, ... - 数値リテラル型 :
1
,3.14
,-2
, ... - 真偽値リテラル型 :
ture
,false
のふたつのみ
型は値の集合でしたが、具体的な値はこのようにリテラルで表現でき、さらにそのリテラルを使ったリテラル型と一対一で対応します。
型は具体的な値の集合としてみなすことができました。リテラル型は具体的な値と一対一の他対応となるので、リテラル型を要素として集合を作ってみると考えてもよいでしょう。たとえば、真偽値のリテラル型は ture
と false
というふたつだけでした。このふたつはリテラル型なので値をひとつしかもたないユニット型です。型が集合だとするとユニット型も集合ですが、このような単一の要素のみからなる集合を単集合(singleton)と言います。というふたつの単集合true
と false
を合成してふたつの型(あるいは値)から和集合を作成するとよく知っている boolean
の型を得ることができます。
true と false の和集合ts
typeBool = true | false;
true と false の和集合ts
typeBool = true | false;
このようにユニオン型で作成した型 Bool
は boolean
型と同一です。
TypeScript には文字列や数値のリテラル型以外にもユニット型が存在しています。たとえば undefined
と null
という値はそれぞれ undefined
型と null
型に属しています。
ts
typeU = undefined;constu :U =undefined ;typeN = null;constn :N = null;
ts
typeU = undefined;constu :U =undefined ;typeN = null;constn :N = null;
逆に undefiend
型と null
型の要素はそれぞれ undefined
と null
しかありません。これらの型は集合としての要素をそれぞれひとつしか持っていないのでユニット型です。
ボトム型
ユニット型が値をひとつしか持たない型なら、値をまったく持たない型も存在しています。そのような型をボトム型(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;
ボトム型が空集合に相当するなら、トップ型は全体集合に相当すると言えるでしょう。
ts
declare constu : unknown;constt : {} | null | undefined =u ;
ts
declare constu : unknown;constt : {} | null | undefined =u ;