はじめに
サバイバルTypeScriptを眺めていたところ、型を「値の集合」と捉えて説明している箇所があり、個人的にわかりやすくて感動しました。
そこで今回は、TypeScfriptの部分型関係などをベン図を使って説明したいと思います。
部分型関係を言葉で説明する
部分型関係とは何かについて、まずは言葉で解説します。
もう一つ参考にさせていただいた「プロを目指す人のためのTypeScript入門」という書籍の言葉を引用すると、
型Sが型Tの部分型であるとは、S型の値がT型の値でもあることを指します。
これだけではピンとこないと思うので、具体例で考えます。
以下のようなHasName型とHasNameAndAge型があるとします。
(結論を先に言うと、HasNameAndAge型の値はHasName型の値でもあるため、HasNameAndAge型はHasName型の部分型です。)
type HasName = {
name: string;
};
type HasNameAndAge = {
name: string;
age: number;
};
ここでHasName型は、string型のnameプロパティを持つオブジェクトの型となります。
要するにHasName型というのは、「string型のnameプロパティを持つ」という条件を満たしているオブジェクトの型を指します。
(ちなみに、name以外のプロパティを持つことについては特に制限されていません。)
同様にHasNameAndAge型というのは、「string型のnameプロパティと、number型のageプロパティを持つ」という条件を満たしているオブジェクトの型を指します。
ここで、「string型のnameプロパティと、number型のageプロパティを持つ」という条件を満たしている場合、
「string型のnameプロパティを持つ」
という条件も必然的に満たされます。
つまり、HasNameAndAge型のオブジェクトは、HasName型のオブジェクトでもあるのです。
よって、HasNameAndAge型はHasName型の部分型となります。
const hasNameAndAgeObj: HasNameAndAge = {
name: "",
age: 0,
};
// hasNameAndAgeObjは、HasNameAndAge型でもあり
const val1: HasNameAndAge = hasNameAndAgeObj;
// HasName型でもある
const val2: HasName = hasNameAndAgeObj;
部分型関係をベン図で表現してみる
ここまでのおさらい
HasName型は、「string型のnameプロパティを持つ」という条件を満たしている型HasNameAndAge型は、「string型のnameプロパティとnumber型のageプロパティを持つ」という条件を満たしている型- つまり、
HasNameAndAge型の条件を満たしていれば、HasName型の条件も満たしている
ここでは、型を「値の集合」として考えてみます。
-
HasNameは、「string型のnameプロパティを持つ」という条件を満たしている値の集合 -
HasNameAndAgeは、「string型のnameプロパティとnumber型のageプロパティを持つ」という条件を満たしている値の集合 -
HasNameAndAgeに属する値は、HasNameにも属する
これをベン図で表現するとこうなります。
まず、図の中で示されている3つのオブジェクトは全て「string型のnameプロパティ」を持っているので、HasName型になります。
そして、{ name: "baz", age: 20, }というオブジェクトは「number型のageプロパティ」も持っているので、HasNameAndAge型にも該当します。
言い換えれば、HasNameAndAge型であるオブジェクトは「string型のnameプロパティ」を持っているので、HasName型にも該当します。
これを集合論っぽく言うと、
「HasNameAndAgeはHasNameに含まれる」、
「HasNameAndAgeはHasNameの部分集合」、
「HasNameAndAge ⊂ HasName」
となります。
つまり、「HasNameAndAge型はHasName型の部分型である」とは、集合論における「HasNameAndAgeはHasNameの部分集合である」に相当します。
集合論がいまいち理解できない方は、以下のYouTubeが参考になるかと思います。
ユニオン型
続いて、ユニオン型を集合論で考えてみます。
ユニオン型とは、「型T または 型U または ...」を表現する型です。
これを集合論で言い換えると、「和集合を表現する型」になります。
type HasName = {
name: string;
};
type HasAge = {
age: number
};
// HasNameOrAge型は、HasName型とHasAge型のユニオン型
type HasNameOrAge = HasName | HasAge
上記のHasNameOrAge型をベン図で表現するとこうなります。
インターセクション型
続いて、インターセクション型を集合論で考えてみます。
インターセクション型とは、「型T でもあり 型U でもあり...」を表現する型です。
これを集合論で言い換えると、「共通部分(積集合)を表現する型」になります。
type HasName = {
name: string;
};
type HasAge = {
age: number
};
// HasNameAndAge型は、HasName型とHasAge型のインターセクション型
type HasNameAndAge = HasName & HasAge
上記のHasNameAndAge型をベン図で表現するとこうなります。
部分型関係の考え方が活きる場面
例えば、関数の引数を指定するときに役立つと思います。
以下、number | undefined 型の引数をもつ関数を考えます。
// 平方根を返す関数
// 実数解があればそれを返し、なければundefinedを返す
const sqrt = (num: number | undefined): number | undefined => {
if (num === undefined) return undefined; // 0は含めないので、num === undefinedで評価
const result = Math.sqrt(num);
if (Number.isFinite(result)) return result;
return undefined;
};
この関数の引数には、number | undefined 型だけではなく、number型やundefined型、さらに169型(数値のリテラル型)の引数も指定可能です。
const val1: number | undefined = 0;
const val2: number = 121;
const val3: undefined = undefined;
const val4: 169 = 169
sqrt(val1); // number | undefined 型は引数に指定可能
sqrt(val2); // number 型も引数に指定可能
sqrt(val3); // undefined 型も引数に指定可能
sqrt(val4); // 169型(数値のリテラル型)も引数に指定可能
なぜならば、number型はnumber | undefined型でもあるから(number型はnumber | undefinedの部分型だから)です。
同様に、undefined型はnumber | undefined型でもあるから(undefined型はnumber | undefinedの部分型だから)です。
そして、196型(数値のリテラル型)もnumber | undefined型であるから(196型はnumber | undefinedの部分型だから)です。
ベン図にすると以下の通りです。
集合論で表現すると、
「number ⊂ number ∪ undefined」
「undefined ⊂ number ∪ undefined」
「169 ⊂ number ∪ undefined」
になるかと思います。
その他
余剰プロパティに対するコンパイルエラー
「部分型関係を言葉で説明する」の節で、以下のようなコードを紹介しました。
type HasName = {
name: string;
};
type HasNameAndAge = {
name: string;
age: number;
};
const hasNameAndAgeObj: HasNameAndAge = {
name: "",
age: 0,
};
// hasNameAndAgeObjは、HasNameAndAge型でもあり
const val1: HasNameAndAge = hasNameAndAgeObj;
// HasName型でもある
const val2: HasName = hasNameAndAgeObj;
しかし、以下のように書くと、コンパイルエラーになります。
const val3: HasName = {
name: "",
age: 0, // コンパイルエラー!
};
オブジェクト リテラルは既知のプロパティのみ指定できます。'age' は型 'HasName' に存在しません。
val3に代入しているオブジェクトの内容は、hasNameAndObjとまったく同じです。
そしてHasName型である変数val2への代入は問題なくできていますが、同じくHasName型である変数val3への代入はコンパイルエラーになってしまいます。
なぜコンパイルエラーになるかというと、どうやら型注釈がある変数にオブジェクトリテラル({と}で定義したオブジェクト)を代入するとき、型注釈にないプロパティがあるとコンパイルエラーが出る仕様になっているかららしいです。
オブジェクトリテラル代入時に余剰プロパティがある場合は、プログラマのミスである可能性が高いため、このような仕様になっているらしいです。
never型
先ほどはnumber | undefined型を扱いましたが、number & undefined型(number型でもありundefined型でもある型)はどうなるかと言うと、
まずnumber & undefined型に該当する値は存在しません。(nullやundefinedも属さない)
なぜならば、先ほどのベン図を見ていただけるとわかる通り、number型とundefined型の共通部分は存在しないからです。
なのでnumber & undefined型は、結果的にnever型という型になります。
never型とは、集合論でいうところの「空集合」と捉えるとわかりやすいかと思います。(つまり、いかなる値も属さない型)
集合論で考えると「number ∩ undefined = ∅(空集合)」になるので、
TypeScriptの型でも「number & undefined型は、never型」となります。
unknown型とany型
集合論でいうところの「空集合」がnever型に相当します。
では、集合論でいうところの「全体集合」に相当する型は何かというと、unknown型らしいです。
なので、unknown型の値にはどんな型の値も入れることができます。
ちなみに、似たような型としてany型もありますが、never型を除くあらゆる型へも割り当て可能な点が特殊です。
const unknown1: unknown = "foo";
const unknown2: unknown = 1;
// 以下2つはコンパイルエラー!
const str: string = unknown1; // 型 'unknown' を型 'string' に割り当てることはできません。
const num: number = unknown2; // 型 'unknown' を型 'number' に割り当てることはできません。
const any1: any = "foo";
const any2: any = 1;
const str: string = any1; // any型をstring型に割り当て可能
const num: number = any2; // any型をnumber型に割り当て可能
参考文献





