2022年3月26日 りあクト! 第4章 TypeScriptで型をご安全に(p.174~)
クラスの2つの顔
TypeScriptには、クラスの型を抽象化して定義する方法が2つあります。
- abstract修飾子を用いて抽象クラスを定義する方法
- インターフェースを使用する方法
abstract修飾子
abstractは抽象クラスを定義する修飾子です。
抽象クラスとは、それ自身がインスタンスを生成出来ず、継承することを前提としたクラスです。
抽象クラスはその定義に実装を含むことができてしまうため、実装を伴った継承を避けるためにもabstract修飾子を用いてクラスの型を抽象化して定義することは推奨されていません。 クラスの実装を伴わずに型だけを適用したい場合にはインターフェースが推奨されています。
インターフェースを用いてクラスの型を抽象化して定義する
インターフェースを用いてクラスの型を定義する例です。 Rectangleクラスの宣言時にimplementsを書くことでインターフェースShape、Quadrangleを適用しています。
interface Shape { readonly name: string; getArea: () => number; } interface Quadrangle { sideA: number; sideB?: number; sideC?: number; sideD?: number; } class Rectangle implements Shape, Quadrangle { readonly name = 'rectangle'; sideA: number; sideB: number; constructor(sideA: number, sideB: number) { this.sideA = sideA; this.sideB = sideB; } getArea = (): number => this.sideA * this.sideB; } const rec = new Rectangle(5, 5); console.log(rec.name); console.log(rec.sideA); console.log(rec.getArea()); // 出力結果 "rectangle" 5 25
- Quadrangle インターフェースの getArea の定義に呼び出し可能オブジェクトの省略記法のアロー構文を使いましたが、getArea(): number という書き方も出来ます。
- アロー構文の場合は、オーバーロードが出来ないため、オーバーロードする予定がないメソッドの型定義はアロー構文で書くことで意図が分かりやすくなると本書では推奨されています。
implements
クラスを宣言するときに、implementsを使うと、そのクラスが特定のインターフェースを満たしていることを表現できます。
以下のコードブロックでは、Catクラス内でメンバー変数colorが定義されていません。そのため、CatクラスがインターフェースAnimalの内容を満たさず、エラーが出ています。
interface Animal { name: string; age: number; color: string; }; class Cat implements Animal { // => Class 'Cat' incorrectly implements interface 'Animal'. // Property 'color' is missing in type 'Cat' but required in type 'Animal'.(2420) name: string; age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } } const cat = new Cat('kevin', 1); console.log(cat);
クラス定義したものをインターフェースのように扱う
class Point { // プロパティ初期化子 // numberを省略しても、0を代入しているので、 // xとyはnumberとして型推論される x: number = 0; y: number = 0; } const pointA = new Point(); // PointはPointインスタンスと同じ構造を持つオブジェクトの型 const pointB: Point = { x: 2, y: 4 }; interface Point3d extends Point { z: number; } const pointC: Point3d = { x: 5, y: 5, z: 10}
- pointAはclass Pointをインスタンス化しています。
- pointBはclass Pointを型定義で使いインターフェースと同じ振る舞いをしています。
- Point3dというインターフェースにclass Pointを継承させています。
TypeScriptではクラス構文を使うとクラスとインターフェースの2つの定義ができており、文脈によって振る舞いが変わります。
コード例のようにコンストラクタ関数を宣言することと、クラスをインターフェースとして扱うことが出来ます。 よって、この書籍では『クラスには2つの顔がある!』と言われています。