Michi's Tech Blog

一人前のWebエンジニアを目指して

TypeScriptによる静的型付け言語入門① 『部分型』

こんにちは!
スマレジ テックファームのMichiです!

6月はTypeScriptキャッチアップ月間として、学んだことを記事にしていきます。

今回はその中でも、考え方にドハマりした『部分型』の考え方について、復習がてら分かりやすく解説します。

部分型とは?

まず、部分型についてざっくり解説します。

部分型とは、ある型が別の型の特性を含んでいるときの関係を表す概念です。もし型Sが型Tの部分型であるならば、型Sは型Tとしても扱うことができます。

これだけ書いても何も分からないと思うので、例を出して説明します。

次のように、生徒の情報を記録するオブジェクトの型: Studentが存在するとします。

type Student = {
  name: string;
  height: number;
};

このStudent型は、名前と身長を記録するプロパティ: nameheightを持ちます。

次に、このStudent型に生徒の誕生日の情報も追加したいという要望があったので、新たにDetailedStudent型も作成しました。

type DetailedStudent = {
  name: string;
  height: number;
  birthday: Date;
};

これは見ての通り、先ほどのStudent型にbirthdayプロパティを追加した型です。

このとき、DetailedStudent型はStudent型の部分型であるということができます。

最初に述べた通り、型Sが型Tの部分型であるとき、型Sは型Tとしても扱うことができます。よって、DetailedStudent型はStudent型の代わりとして使うことができます。

const john: DetailedStudent = {
  name: "John",
  height: 170,
  birthday: new Date('2005-12-17'),
};

const student: Student = john; // DetailedStudentはStudentの部分型なので、この代入は可能。

console.log(student.name); // "John"
console.log(student.height); // 170

なぜこれが可能なのかというと、DetailedStudent型はStudent型のプロパティをすべて持っているからです。

Student型のプロパティはnameheightの2つだけです。そして、DetailedStudent型はこれらのプロパティを2つとも持っています。

このとき、「あれ、Student 型はbirthdayを持っていないけど、それはいいの?」と思われる方もいるかもしれません。これを考えるために、Student型の定義についてもう一度見直してみましょう。

Studentは『nameプロパティがstring型で、かつheightプロパティがnumber型であるオブジェクト』の型です。

ここで注目してほしいのは、nameheightを以外のプロパティには言及していないということです。実は、nameheight以外のプロパティは持っていても持っていなくても構わないのです。

よって、DetailedStudent型はStudent型の代わりに使うとこができる、要するに、DetailedStudent型はStudent型の部分型であるということができます。

反対に、Student型のオブジェクトをDetailedStudent型の変数に代入しようとすると、TypeScriptはエラーを発生させます。それはStudent型がDetailedStudent型の全てのプロパティ(この場合はbirthday)を持っていないためです。

const lisa: Student = {
  name: "Lisa",
  height: 160
};

const detailedStudent: DetailedStudent = lisa; // これはエラーになる

わかりやすいように、イメージ図を作ってみました。

DetailedStudent型はStudent型に含まれているので、DetailedStudent型はStudent型であると言い換えることができますね。オブジェクト指向継承の概念によく似ています。

余談:なぜStudent型のbirthdayプロパティにアクセスできないのか

先ほど、「Student型はnameheight以外のプロパティは持っていても持っていなくても構わない」と述べました。そこで、TypeScriptを少し触ったことのある方ならこう思うでしょう。

nameheight以外のプロパティにアクセスしようとするとエラーになるけど、矛盾してない?」

実際に、次のコードはエラーとなります。

type Student = {
  name: string;
  height: number;
};

const mike: Student = {name: "Mike", height: 178}

console.log(mike.birthday)
// エラー: Property 'birthday' does not exist on type 'Student'.

Student型はbirthdayプロパティを持っているかもしれないのに、実際はTyepScriptによって、birthdayへアクセスすることが防がれます。

これは、持っているか持っていないか分からないプロパティに触るのは危険でありミスの可能性が高いので、TypeScriptのおっせかいで、明示的に宣言しているnameheight以外のプロパティにアクセスするのはコンパイルエラーによって防がれているのです。

また、同じような理由から次のように、存在するか分からないプロパティに代入を行おうとするコードもエラーとなります。

type Student = {
  name: string;
  height: number;
};

const mike: Student = {name: "Mike", height: 178, birthday: new Date('2001-05-09'),}
// エラー: Type '{ name: string; height: number; birthday: Date; }' is not assignable to type 'Student'.
// Object literal may only specify known properties, and 'birthday' does not exist in type 'Student'.

一個前のコードのように、存在するかどうか分からないプロパティ: birthdayにアクセスすることは許されません。

どうせアクセスできないのなら、代入しても使い道がないので、まったく無意味なプログラムです。無意味なプログラムはミスである可能性が高いですから、TypeScriptがおっせかいをして、このようなエラーを出してくれているのです。

まとめ

TypeScriptの記事第一弾は、部分型についてでした。

自分は静的型付け言語を触るのが初めてで、この部分型の概念を理解するのにかなり苦労しました。ただ、オブジェクト指向の継承の概念と似ているということに気づいてからは、割とすんなり理解することができました。

本当は、関数の部分型についても書きたかった(ここは理解するのにもっと苦労した)のですが、そこまで書くとあまりにも長くなりすぎるので、今回はこのあたりで。機会があれば、関数の部分型についても記事にしようと思います。