はじめに
サバイバル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型に割り当て可能
参考文献