【技術書記録005-3 クラスとインターフェース】プログラミングTypeScript スケールするJavaScriptアプリケーション開発

TypeScriptオライリー本第3弾、クラスとインターフェースの章。
クラス、抽象クラス、インターフェース、型エイリアスの違い等についてまとめる。

クラスのアクセス修飾子等

public どこからでもアクセス可能。デフォルトはこれ
protected このクラスとサブクラスのインスタンスからアクセス可能
private このクラスのインスタンスのみアクセス可能
readonly インスタンスプロパティを読み取りのみ可能・書き込み不可にする
static インスタンスを生成しなくても、そのクラスのプロパティ・メソッドを使用できるようになる
abstract 抽象クラス・抽象メソッドに指定する。そのサブクラスに対して抽象メソッドの実装を強制する

抽象クラスの拡張の例。

abstract class MicrowaveOven {
  private power: number
  abstract heat(time: number, food: string): void
}

class MyMicrowaveOven extends MicrowaveOven {
  heat(time, food) {
    console.log(`${food}${time}分あたためる`)
  }
}

エイリアスとインターフェースの違い

エイリアス インターフェース
右辺に指定できる型 任意の型・型演算子 形状のみ
拡張元から拡張先への割り当て 自動的に両者の型を結合し、シグネチャオーバーロードする できない
宣言のマージ できない 自動的に行われる

1つ目の違いの具体例。インターフェースは{}で囲われた形のみ可能。型エイリアスの方がより柔軟に書ける。

type Girl = string
type Female = Girl | number

interface Soda {
  tansan: string
}
interface Cola extends Soda {
  spice: string
}


2つ目の違いの具体例。本書指示どおり書いてみる。まずインターフェース。

interface A {
  good(x: number): string
  bad(x: number): string
}
interface B extends A {
  good(x: string | number): string
  bad(x: string): string
}

// インターフェイス 'B' はインターフェイス 'A' を正しく拡張していません。
// プロパティ 'bad' の型に互換性がありません。
// 型 '(x: string) => string' を型 '(x: number) => string' に割り当てることはできません。
// パラメーター 'x' および 'x' は型に互換性がありません。
// 型 'number' を型 'string' に割り当てることはできません。

次に型エイリアス。こちらはエラーが出ず通ってしまう。
オブジェクト型の継承を行うときは、エラーがチェックできるインターフェースを使用する方が安全。

type A = {
  good(x: number): string
  bad(x: number): string
}
type B = A & {
  good(x: string | number): string
  bad(x: string): string
}


3つ目の違いの具体例。インターフェースの場合、同じ名前を複数宣言すると、自動的にプロパティがマージされる。
一方、型エイリアスは同じ名前を重複して宣言できない。

interface House {
  garden: boolean
}
interface House {
  parking: boolean
}

let myHouse: House = {
  garden: false,
  parking: false
}
type Room = {
  size: number
}
type Room = {
  height: number
}
// 識別子 'Room' が重複しています。

クラスの実装

クラスはインターフェース・型エイリアスを実装できる。
複数実装できるし、両者を同時に実装することも可能。 実装が抜けている箇所があれば、TypeScriptがエラーを教えてくれる。

インターフェースと型エイリアスの複数実装の例。

type Human = {
  run(): void
  cook(food: string): void
}
interface Gorilla {
  power: number
}

class Watasi implements Human, Gorilla {
  power = 100

  run() {
    console.log('走る!')
  }
  cook(food) {
    console.log(`${food}を料理`)
  }
}

「インターフェースの実装」と「抽象クラスの拡張」の違い

インターフェースの実装 抽象クラスの拡張
ランタイムコードの発行 しない(ので軽量) する(つまりJavaScriptのクラス)
汎用性 高い。形状をモデル化。配列、関数、クラス、クラスインスタンスを表現できる 低い。クラスのモデル化に特化
アクセス修飾子 使用不可 使用可

複数のクラスで実装を共有し、継承させる場合は抽象クラスを、「このクラスはTである」と表現するための軽量な方法が必要な場合はインターフェースを使用する。

ジェネリック

クラスもインターフェースもジェネリックが使える。
クラスの具体例。

class Soccer<T, U> {
  constructor(
    ball: T,
    goal: U
  ) {}

  shoot(ball: T, goal: U): void {
    // ...
  }
  
  static watch<T, U>(game: T, date: U): Soccer<T, U> {
    return // ...
  }
}

静的メソッドはクラスのジェネリックにアクセスできないので、代わりに独自のジェネリックを宣言している。
インターフェイスの具体例。

interface Baseball<T, U> {
  hit(ball: T): U
}

finalクラスをシミュレートする

TypeScriptはfinalキーワードをサポートしていないが、コンストラクタにprivateを追加することでシミュレート可能。

class BallpointPen {
  private constructor(private ink: number) {}
}

class OilBasedBallpointPen extends BallpointPen {}
// クラス 'BallpointPen' を拡張できません。Class コンストラクターがプライベートに設定されています。

new BallpointPen(50)
// クラス 'BallpointPen' のコンストラクターはプライベートであり、クラス宣言内でのみアクセス可能です。

しかしこれだと、インスタンス化までできなる。そこで専用の静的メソッドを追加する

class BallpointPen {
  private constructor(private ink: number) {}
  static create(ink: number) {
    return new BallpointPen(ink)
  }
}

class OilBasedBallpointPen extends BallpointPen {}
// クラス 'BallpointPen' を拡張できません。Class コンストラクターがプライベートに設定されています。

BallpointPen.create(50)



今回はここまで。
インターフェースは同じ名前で複数宣言するとマージされちゃうので、exportするときなど気をつけないと。
プリミティブ型やリテラルのユニオン型とかを宣言できるので、型エイリアスの方がインターフェースより使い勝手が良いかもしれない。
次回は「高度な型」についての予定。