2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

サバイバル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にも属する

これをベン図で表現するとこうなります。

image.png

まず、図の中で示されている3つのオブジェクトは全て「string型のnameプロパティ」を持っているので、HasName型になります。
そして、{ name: "baz", age: 20, }というオブジェクトは「number型のageプロパティ」も持っているので、HasNameAndAge型にも該当します。

言い換えれば、HasNameAndAge型であるオブジェクトは「string型のnameプロパティ」を持っているので、HasName型にも該当します。

これを集合論っぽく言うと、
HasNameAndAgeHasNameに含まれる」、
HasNameAndAgeHasNameの部分集合」、
HasNameAndAgeHasName
となります。

つまり、「HasNameAndAge型はHasName型の部分型である」とは、集合論における「HasNameAndAgeHasNameの部分集合である」に相当します。


集合論がいまいち理解できない方は、以下のYouTubeが参考になるかと思います。

ユニオン型

続いて、ユニオン型を集合論で考えてみます。

ユニオン型とは、「型T または 型U または ...」を表現する型です。
これを集合論で言い換えると、「和集合を表現する型」になります。

type HasName = {
  name: string;
};

type HasAge = {
  age: number
};

// HasNameOrAge型は、HasName型とHasAge型のユニオン型
type HasNameOrAge = HasName | HasAge

上記のHasNameOrAge型をベン図で表現するとこうなります。

image.png

image.png

インターセクション型

続いて、インターセクション型を集合論で考えてみます。

インターセクション型とは、「型T でもあり 型U でもあり...」を表現する型です。
これを集合論で言い換えると、「共通部分(積集合)を表現する型」になります。

type HasName = {
  name: string;
};

type HasAge = {
  age: number
};

// HasNameAndAge型は、HasName型とHasAge型のインターセクション型
type HasNameAndAge = HasName & HasAge

上記のHasNameAndAge型をベン図で表現するとこうなります。

image.png

image.png

部分型関係の考え方が活きる場面

例えば、関数の引数を指定するときに役立つと思います。

以下、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の部分型だから)です。

ベン図にすると以下の通りです。

image.png

image.png

集合論で表現すると、
numbernumberundefined
undefinednumberundefined
169numberundefined
になるかと思います。

その他

余剰プロパティに対するコンパイルエラー

「部分型関係を言葉で説明する」の節で、以下のようなコードを紹介しました。

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型に該当する値は存在しません。(nullundefinedも属さない)
なぜならば、先ほどのベン図を見ていただけるとわかる通り、number型とundefined型の共通部分は存在しないからです。

なのでnumber & undefined型は、結果的にnever型という型になります。

never型とは、集合論でいうところの「空集合」と捉えるとわかりやすいかと思います。(つまり、いかなる値も属さない型)
集合論で考えると「numberundefined = ∅(空集合)」になるので、
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型に割り当て可能

参考文献

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?