🌊

TypeScriptのabstractとinterfaceを比べてみた

2022/02/26に公開

オブジェクトの型を指定するインターフェースに関してですが、クラスに関しても使えるということで、特に抽象クラスの定義について色々と調べてみました。

TypeScriptにおけるクラスとは

実はTypeScriptでは、JSよりも前にクラスを備えていました。
特徴として、TypeScriptのクラス構文では2つの異なる側面を併せ持ちます。

インターフェースとしての型宣言となる

型のコンテキストにおいてはインターフェースとして扱われます。

コンストラクタ関数(通常のクラスとして)

コンストラクタ関数とかくとややこしいですが要は普通のクラス構文として振る舞うということです。JavaScript, TypeScriptではクラスはシンタックスシュガーであり、その実態はプロトタイプオブジェクトを継承してオブジェクトインスタンスを生成するための独立した関数となっています。

抽象クラスの定義

クラスが宣言されたとき、インターフェースとしての側面と通常のクラスとしての側面を併せ持つと言いました。よって抽象クラスにも2つの定義の仕方が存在します。
抽象クラスとはそれ自身がインスタンスを生成できず、継承されることを前提としたクラスです。

abstract修飾子

まずは通常のクラスの扱いとしての見慣れているやり方ですね。抽象クラスをabstract修飾子で定義し、それをextendsによって継承して実装するというものです。

abstract class Rectangle {
  sideA: number;
  sideB: number;

  constructor(sideA: number, sideB: number) {
    this.sideA = sideA;
    this.sideB = sideB;
  }

  getArea = (): number => this.sideA * this.sideB;
}

class Square extends Rectangle {
  side: number;
  constructor(side: number) {
    super(side, side);
  }
}

ポイント

  • 実装の詳細を抽象クラスでかけてしまう
  • 継承先で再実装しなくてもエラーにならない

interface

初めに、クラスを定義したときはそれは型のコンテキストではインターフェースの定義と同等となるという説明をしました。
つまりクラスはinterfaceを継承することができます。
implementsを使用します。

interface Shape {
  readonly name: string;
  sideA: number;
  sideB?: number;
  sideC?: number;
  sideD?: number;
  getArea: () => number;
}

class Rectangle implements Shape {
  readonly name = 'rectangle';

  sideA: number;

  sideB: number;

  constructor(sideA: number, sideB: number) {
    this.sideA = sideA;
    this.sideB = sideB;
  }

  getArea = (): number => this.sideA * this.sideB;
}

インターフェースはもともとオブジェクトの型定義に使用されるものですがJavaScriptのスーパーセットであるTypeScriptではJavaScript同様にクラスとオブジェクトの間に本質的な区別はないからです。
クラスももとを辿ればオブジェクト。なのでインターフェースで型定義することができ、またそれを継承することができます。

ポイント

  • 型だけを定義し、実装漏れを確実に防ぐ
  • ?によって柔軟性をもたせることができる
  • 実装の詳細を継承元で書かなくて済む

抽象クラスの弱点と interfaceの良さ

abstract修飾子は、実は実装の詳細を含むことができてしまいます。本当は実装すべきメンバの型だけを定義したいのにも関わらずです。これが唯一のabstract修飾子による抽象クラス定義の弱点でしょうか。

DRYでコードを書くことはRubyでも口酸っぱく言われていますが、継承というのは親に強く依存してしまいバグの発見がしづらくなるという理由から「継承よりも合成を使う」という考え方のほうが主流になりつつあるようです。
比較的新しい言語であるGoやRustでは実装を伴った継承がそもそも存在しません。

その点インターフェースでは実装を伴わない型のみの定義が可能であり、抽象クラスの定義として優れています。

結論

よほどのことがない限り、クラスの定義はinterfaceを使ったほうが良いのではないのでしょうか。
その「よほどのこと」というのも現時点では私の中では出てきていないので、もしユースケースがあれば教えていただきたいです!

ではでは、キラキラTypeScriptライフをお過ごしください。

Discussion