メインコンテンツまでスキップ

型のメンタルモデル

型システムの背景理論

プログラミング言語の型システムにはそれぞれ固有の世界観があり、言語ごとに型の機能が異なります。

その一方で複数の言語で共通している機能もあるわけで、それらのさまざまな型の機能は唐突にどこからともなく出現してきたわけではありません。背景として大きくは型理論(type theory)と呼ばれる数学的な研究分野があり、各言語の型システムは型理論に基づいて実装されています。

たとえば、TypeScriptのunkown型やnever型のような一見何のためにあるかわからないような型であっても、型理論においてはその役割や機能を一般的に説明することができます。これらの型はトップ型やボトム型と呼ばれる型の種類に分類され、部分型関係の両端点に位置する型として振る舞います。

このような型理論的な観点からの知識を持つことで似たような型システムを持つ他の言語においても型の機能について自然に推論することが可能になります。たとえばScalaという言語ではAny型とNothing型がunknown型とnever型と同じ働きをすることが推論できます。一般化された型についての知識を使えるため、プログラミング言語をスイッチするような場合においてもスムーズに機能の類推や学習を行うことができるようになります。

型理論は非常に奥深く難解でもありますが、その一方で比較的簡単に理解できて実用的にも役立つ概念も非常に多くあります。このドキュメントではそういった知識からTypeScriptの型の世界観、いわばメンタルモデルを構築するための知識の一部を紹介します。

集合論的なデザイン

型のメンタルモデル、つまり「型をどのように解釈するか」を考える上で非常に有用な数学的なツールがあります。それが集合論(set thoery)であり、この章では「型=集合」として考えることにします。

一般に型(type)は集合(set)は異なる概念ですが、型理論と集合論の間には密接な関連があります。

特にTypeScriptにおいては、型を集合論的に扱えるようなデザインが意図的になされており、型を「値の集合」として捉えることで直感的に型を理解することができるようになっています。この見方は決して偏ったものではなく、公式ドキュメントでも推奨されている型の考え方です。

本章では、このような集合論的な見方に立って型を考えることで、型の振る舞いについての自然な推論を行えるようなメンタルモデルを構築します。

集合演算

型を集合論的に扱えるお陰で、TypeScriptの型は集合が持つような演算の一部を利用することができます。

集合の演算は集合から新しい集合を作り出すような操作であり、そのような演算にはいくつも種類がありますが、TypeScriptでは和集合と共通部分を作り出すことができる演算が備わっています。

ユニオン型インターセクション型はまさに和集合と共通部分を作る演算に相当します。

型の和集合と共通部分
ts
type A = { fst: string };
type B = { snd: number };
 
type Union = A | B;
type Intersection = A & B;
型の和集合と共通部分
ts
type A = { fst: string };
type B = { snd: number };
 
type Union = A | B;
type Intersection = A & B;

直感的にはユニオン型はふたつの集合の和を表現する型であり、インターセクション型はふたつの型の共通部分を表現する型です。ユニオン型は型の絞り込みなどにおいて特に重要な役割を果たします。

このふたつの型は複数の型から新しい型を合成できるという点で演算として重要ですが、特定の型そのものが集合としてどのように解釈できるかを次に紹介する3つの型で解説していきます。

ユニット型

ここからは、TypeScriptにおいて型は値の集合として扱えることができることを具体例を交えて説明していきます。

まずは単に型を「値の集合」であると考えてください。たとえば、number型という数値を表す型ですが、この型が集合であるとすると、その要素は具体的なnumber型の値である数値です。たとえば13.14などの数値がこの集合の要素となります。number型の章で述べたようにnumber型で表現可能な範囲は有限であり、それらの範囲の要素にNaNInfinityなどの特殊な定数を加えた集合がnumber型の集合ということになります。

さて、重要な型の概念として、ユニット型(unit type)という型の種類があります。ユニット型とは文字通りの単位的な型であり、型の要素として値をひとつしか持たないような型です。集合論においては単一の要素からなる集合は単位集合(unit set)や単集合(singleton)など呼ばれます。

型の世界での単位集合に相当するものがユニット型であり、たとえば、PHPではNullというひとつの値を持つnull型が、Javaではnullというひとつの値を持つVoid型がそれぞれユニット型に相当します。KotlinやScalaでは分かりやすくUnit型という名前になっています。

TypeScriptではnullというひとつの値を持つnull型と、undefinedというひとつの値を持つundefined型がユニット型に相当します。

ts
type U = undefined;
const u: U = undefined;
 
type N = null;
const n: N = null;
ts
type U = undefined;
const u: U = undefined;
 
type N = null;
const n: N = null;

さらに思い出してほしいのは、TypeScriptにはリテラル型という型がありました。TypeScriptではこのリテラル型もユニット型に相当します。

リテラル型はユニット型
ts
type Unit = 1;
const one: Unit = 1;
リテラル型はユニット型
ts
type Unit = 1;
const one: Unit = 1;

リテラル型は値リテラルをそのまま型として表現できる型であり、numberstringなどのプリミティブ型にはそれぞれ具体的な値のリテラルによって作成されるリテラル型が存在します。

  • 文字列リテラル型 : "st", "@", ...
  • 数値リテラル型 : 1, 3.14, -2, ...
  • 真偽値リテラル型 : ture, false のふたつのみ

型は値の集合でしたが、具体的な値はこのようにリテラルで表現でき、さらにそのリテラルを使ったリテラル型と一対一で対応します。

集合の要素の個数は「濃度(cardinality)」と呼ばれる概念によって一般化され、基数という数によって表記されます。たとえば、要素がひとつしかない単位集合の濃度は1です。つまり、型を集合としてみなしたときのユニット型の濃度は1ということになります。

それでは濃度が2、つまり要素の個数が二個からなるシンプルな型について考えてみましょう。たとえば、真偽値を表す boolean という型の要素(値)はtruefalseのみであり、boolean型の変数にはそれら以外の値を割り当てることはできません。したがってboolean型は濃度2の集合としてみなせます。

ts
const b1: boolean = true;
const b2: boolean = false;
const b3: boolean = 1;
Type 'number' is not assignable to type 'boolean'.2322Type 'number' is not assignable to type 'boolean'.
ts
const b1: boolean = true;
const b2: boolean = false;
const b3: boolean = 1;
Type 'number' is not assignable to type 'boolean'.2322Type 'number' is not assignable to type 'boolean'.

リテラル型について思い出すと真偽値についてもそれぞれリテラル型truefalseが存在しました。これらの型はそれぞれがひとつの値だけを持つユニット型でした。

リテラル型は具体的な値と一対一の他対応となります。型の集まりには集合演算が備わっていたので、リテラル型を要素として新しい集合を作ってみると考えてもよいでしょう。ふたつの単集合truefalseを合成してふたつの型(あるいは値)から和集合を作成すると濃度2の型を得ることができます。

true と false の和集合
ts
type Bool = true | false;
true と false の和集合
ts
type Bool = true | false;

このようにユニオン型で合成した型Boolboolean型と同一になります。

ボトム型

ユニット型が値をひとつしか持たない型なら、値をまったく持たない型も存在しています。そのような型をボトム型(bottom type)と呼びます。型が集合であるとするとき、ボトム型は空集合(empty set)に相当し、空型(empty type)とも呼ばれることがあります。

ボトム型は値をまったく持たない型として、例外が発生する関数の返り値の型として利用されますが、TypeScriptでのボトム型はまさにnever型です。

ts
function neverReturn(): never {
throw new Error("決して返ってこない関数");
}
ts
function neverReturn(): never {
throw new Error("決して返ってこない関数");
}

never型は集合としては空集合であり、値をひとつも持たないため、その型の変数にはどのような要素も割り当てることができません。

ts
const n: never = 42;
Type 'number' is not assignable to type 'never'.2322Type 'number' is not assignable to type 'never'.
ts
const n: never = 42;
Type 'number' is not assignable to type 'never'.2322Type 'number' is not assignable to type 'never'.

トップ型

ボトム型が値をまったく持たない型なら、すべての値を持つような型も存在しています。そのような型をトップ型(top type)と呼びます。

トップ型はすべての値をもっており、その型の変数にはあらゆる値を割り当てることができます。オブジェクト指向言語であれば大抵は型階層のルート位置に存在している型であり、TypeScriptではunknown型がトップ型に相当します。

ts
const u1: unknown = 42;
const u2: unknown = "st";
const u3: unknown = { p: 1 };
const u4: unknown = null;
const u5: unknown = () => 2;
ts
const u1: unknown = 42;
const u2: unknown = "st";
const u3: unknown = { p: 1 };
const u4: unknown = null;
const u5: unknown = () => 2;

ボトム型が空集合に相当するなら、トップ型は全体集合に相当すると言えるでしょう。TypeScriptはunknown型を{} | null | undefindというユニオン型相当として扱い、相互に割当可能としています。

ts
declare const u: unknown;
const t: {} | null | undefined = u;
ts
declare const u: unknown;
const t: {} | null | undefined = u;

{}はプロパティを持たないオブジェクトを表現する空のobject型であり、この型はあらゆるオブジェクトの型とnullundefinedを除くすべてのプリミティブ型を包含しています。したがって、unknownという全体集合は上記のような3つの集合に分割できると考えることもできます。

TypeScriptにはunknwon型以外にもうひとつ特殊なトップ型があります。それがany型です。

unknown型は部分的関係において純粋なトップ型として機能していますが、any型はあらゆる型からの割当が可能だけでなく、never型を除くあらゆる型へも割当可能なため一見するとボトム型のように振る舞っているように感じられますが、ボトム型ではありません。

ts
declare const a: any;
 
const n1: unknown = a;
const n2: {} = a;
const n3: number = a;
const n4: 1 = a;
const n5: never = a;
Type 'any' is not assignable to type 'never'.2322Type 'any' is not assignable to type 'never'.
ts
declare const a: any;
 
const n1: unknown = a;
const n2: {} = a;
const n3: number = a;
const n4: 1 = a;
const n5: never = a;
Type 'any' is not assignable to type 'never'.2322Type 'any' is not assignable to type 'never'.

TypeScriptは元来、JavaScriptに対してオプショナルに型付けを行うという言語であり、型付けを行わない場合には未知の型をany型として推論します。このような状況において、any型はあらゆる型からの割当が可能であるだけでなく、あらゆる型への割当が可能であることが必要であり、それによって型注釈がないJavaScriptに対して漸進的に型を付けていくことが可能になります。

実はany型はunknown型がTypeScriptに導入されるまで唯一のトップ型として機能していましたが、純粋にあらゆる型の上位型になる部分型関係のトップ位置の型として機能するunknown型が導入されたことで部分型関係の概念が明瞭になりました。

部分型関係の解釈

そもそも部分型関係は「型Aが型Bの部分型であるとき、Bの型の値が求められる際にAの型の値を指定できる」という関係です。関数型を除いて通常の型については型を集合として解釈すれば、部分型関係は集合の包含関係に相当します。

unknown型はあらゆる型に対して上位型として振る舞い、あらゆる型はunknown型の部分型となります。すなわち、型を集合として解釈したとき、unknown型はTypeScriptにおけるあらゆる値を含む集合であり、あらゆる型はunknown型の部分集合となります。