TypeScriptのclassは、2つの役割がある。
- コンストラクタのシグネチャを定義をする
- クラスのインスタンスの型をインターフェイスとして定義をする
classとinterfaceの線引きが比較的明瞭な言語(JavaやPHPなど)からすると、前者はすんなり理解できるものだが、後者には意外性があるかもしれない。「classを書いたら、interfaceも書いたことになる」ということだからだ。
TypeScriptのclass定義の振る舞いをひとつひとつ確認していこう。
ここに、2つの属性を持ったRectクラスの定義がある。
class Rect {
x: number = 0
y: number = 0
}
new クラス名がコンパイルに通るのは、1つ目の「コンストラクタのシグネチャを定義をする」役割によるものである。これは、クラスを使ったことがある経験があれば、何の不思議もないだろう。
const rect = new Rect()
2つ目の役割である「クラスのインスタンスの型をインターフェイスとして定義をする」ことは、TypeScriptの興味深い仕様だ。先のclass Rectのコードの裏で、次のような型定義がなされている:
interface Rect {
x: number
y: number
}
型アノテーションでRectが使えるのは、そのためだ。次のコードは、new Rect()の戻り値がRect型であることを表現した一文であるが、これ自体は別段不思議はないだろう:
const rect: Rect = new Rect()
^^^^
これと似たようなコードではあるが、次のサンプルコードはTypeScriptらしさがより強く分かる。
const rect: Rect = { x: 3, y: 3 }
^^^^^^^^^^^^^^
rectは型アノテーションでRect型であることを宣言しているが、その値はRectクラスではない。あくまで、Rectインターフェイスを実装したオブジェクトである。このコードはコンパイルがちゃんと通る。
ここまでで、Rectがインターフェイスであることが分かったので、もっとインターフェイスさがよく分かる例を見ていこう。
Rectはインターフェイスなので、普通のinterfaceと同様に、クラスでimplementsすることも当然のようにできる。
class Cube implements Rect {
^^^^^^^^^^
x: number = 0
y: number = 0
z: number = 0
}
const rect: Rect = new Cube()
extendsと違うところは、CubeがRectを継承しているわけではないというところだ。
Rectはインターフェイスなので、普通のinterfaceの親にすることができる:
interface Cube extends Rect {
^^^^^^^^^ ^^^^^^^
z: number
}
const cube: Cube = { x: 1, y: 2, z: 3 }
const rect: Rect = cube
Rectはインターフェイスなので、普通のinterface同様にopen-endedの特徴も持っている。したがって、classを定義とは別に、クラスと同名のinterfaceを定義することで、Declaration Mergingを発生させることもできる。
class Rect {
x: number = 0
y: number = 0
}
// 追加のインターフェイス定義
interface Rect {
area(): number
}
上のコードでは、Rectに追加でarea()メソッドを定義している。
もちろん、これだけではarea()メソッドの実装が存在していないので、Rectクラスをnewしただけの次のようなコードでは、コンパイルは通るものの当然ながらランタイムエラーになる。
const rect = new Rect()
rect.area() // runtime error: Uncaught TypeError: rect.area is not a function
所感
TypeScriptのclass = インスタンスのインターフェイス定義 + コンストラクタの定義
— suin❄️Terraformエンジニア募集中 (固定ツイ参照) (@suin) November 4, 2019
「classを書くと、暗黙的にinterfaceも定義している」ということになる。
これは、他の言語からすると「意外さ」でもあると同時に、TypeScriptの柔軟さを高める要素でもあるような気がする。 pic.twitter.com/kMSNZWCLmy