TypeScriptに取り組む
この本をサポートしてください: 購入 または 寄付
(広告、ブロックしないでください。)


このTypeScriptの章では、クラスとそのインスタンスに関連する型を調べます。

17.1 クラスの2つのプロトタイプチェーン

次のクラスを考えてみましょう

class Counter extends Object {
  static createZero() {
    return new Counter(0);
  }
  value: number;
  constructor(value: number) {
    super();
    this.value = value;
  }
  increment() {
    this.value++;
  }
}
// Static method
const myCounter = Counter.createZero();
assert.ok(myCounter instanceof Counter);
assert.equal(myCounter.value, 0);

// Instance method
myCounter.increment();
assert.equal(myCounter.value, 1);
Figure 2: Objects created by class Counter. Left-hand side: the class and its superclass Object. Right-hand side: The instance myCounter, the prototype properties of Counter, and the prototype methods of the superclass Object..

2の図は、クラスCounterのランタイム構造を示しています。この図には、オブジェクトの2つのプロトタイプチェーンがあります。

この章では、最初にインスタンスオブジェクト、次にオブジェクトとしてのクラスを探ります。

17.2 クラスのインスタンスに対するインターフェース

インターフェースは、オブジェクトが提供するサービスを指定します。例:

interface CountingService {
  value: number;
  increment(): void;
}

TypeScriptのインターフェースは構造的に機能します。オブジェクトがインターフェースを実装するためには、正しい型を持つ適切なプロパティを持っているだけで済みます。次の例でそれを見ることができます。

const myCounter2: CountingService = new Counter(3);

構造的なインターフェースは、既存のオブジェクトに対してもインターフェースを作成できるため便利です(つまり、後から導入できます)。

オブジェクトが特定のインターフェースを実装する必要があることが事前にわかっている場合は、後で驚くことを避けるために、早期に実装しているかどうかを確認するのが理にかなっています。これは、implementsを介してクラスのインスタンスに対して行うことができます。

class Counter implements CountingService {
  // ···
};

コメント

17.3 クラスに対するインターフェース

クラス自体もオブジェクト(関数)です。したがって、インターフェースを使用してそのプロパティを指定できます。ここでの主なユースケースは、オブジェクトのファクトリを記述することです。次のセクションで例を示します。

17.3.1 例:JSONとの変換

次の2つのインターフェースは、インスタンスのJSONとの変換をサポートするクラスに使用できます。

// Converting JSON to instances
interface JsonStatic {
  fromJson(json: any): JsonInstance;
}

// Converting instances to JSON
interface JsonInstance {
  toJson(): any;
}

次のコードでこれらのインターフェースを使用します。

class Person implements JsonInstance {
  static fromJson(json: any): Person {
    if (typeof json !== 'string') {
      throw new TypeError(json);
    }
    return new Person(json);
  }
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  toJson(): any {
    return this.name;
  }
}

これは、クラスPerson(オブジェクトとして)がインターフェースJsonStaticを実装しているかどうかをすぐに確認する方法です。

// Assign the class to a type-annotated variable
const personImplementsJsonStatic: JsonStatic = Person;

次のチェック方法は良い考えのように思えるかもしれません。

const Person: JsonStatic = class implements JsonInstance {
  // ···
};

ただし、実際には機能しません。

17.3.2 例:TypeScriptの組み込みインターフェース(クラスObjectとそのインスタンス用)

TypeScriptの組み込み型を見てみましょう。

一方、インターフェースObjectConstructorはクラスObject自体用です。

/**
 * Provides functionality common to all JavaScript objects.
 */
declare var Object: ObjectConstructor;

interface ObjectConstructor {
  new(value?: any): Object;
  (): any;
  (value: any): any;

  /** A reference to the prototype for a class of objects. */
  readonly prototype: Object;

  /**
   * Returns the prototype of an object.
   * @param o The object that references the prototype.
   */
  getPrototypeOf(o: any): any;

}

他方、インターフェースObjectObjectのインスタンス用です。

interface Object {
  /** The initial value of Object.prototype.constructor is the standard built-in Object constructor. */
  constructor: Function;

  /** Returns a string representation of an object. */
  toString(): string;
}

名前Objectは、2つの異なる言語レベルで2回使用されます。

17.4 型としてのクラス

次のクラスを考えてみましょう。

class Color {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

このクラス定義は、次の2つのものを作成します。

まず、Colorという名前のコンストラクター関数(newで呼び出すことができます)。

assert.equal(
  typeof Color, 'function')

次に、Colorのインスタンスに一致するColorという名前のインターフェース。

const green: Color = new Color('green');

これが、Colorが実際にインターフェースである証拠です。

interface RgbColor extends Color {
  rgbValue: [number, number, number];
}

17.4.1 落とし穴:クラスは構造的に機能し、名目的に機能しない

ただし、1つの落とし穴があります。Colorを静的型として使用することは、それほど厳密なチェックではありません。

class Color {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}
class Person {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

const person: Person = new Person('Jane');
const color: Color = person; // (A)

なぜTypeScriptはA行で文句を言わないのでしょうか?それは構造的型付けによるものです。PersonColorのインスタンスは同じ構造を持ち、したがって静的に互換性があります。

17.4.1.1 構造的型付けをオフにする

プライベートプロパティを追加することにより、オブジェクトの2つのグループを非互換にすることができます。

class Color {
  name: string;
  private branded = true;
  constructor(name: string) {
    this.name = name;
  }
}
class Person {
  name: string;
  private branded = true;
  constructor(name: string) {
    this.name = name;
  }
}

const person: Person = new Person('Jane');

// @ts-expect-error: Type 'Person' is not assignable to type 'Color'.
//   Types have separate declarations of a private property
//   'branded'. (2322)
const color: Color = person;

この場合、プライベートプロパティは構造的型付けをオフにします。

17.5 参考文献