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

16 TypeScriptにおけるクラス定義



この章では、TypeScriptでのクラス定義の仕組みを検証します。

16.1 チートシート: プレーンなJavaScriptでのクラス

このセクションは、プレーンなJavaScriptでのクラス定義のチートシートです。

16.1.1 クラスの基本的なメンバー

class OtherClass {}

class MyClass1 extends OtherClass {

  publicInstanceField = 1;

  constructor() {
    super();
  }

  publicPrototypeMethod() {
    return 2;
  }
}

const inst1 = new MyClass1();
assert.equal(inst1.publicInstanceField, 1);
assert.equal(inst1.publicPrototypeMethod(), 2);

  次のセクションでは、修飾子について説明します

最後に、修飾子を組み合わせる方法を示す表があります。

16.1.2 修飾子: static

class MyClass2 {

  static staticPublicField = 1;

  static staticPublicMethod() {
    return 2;
  }
}

assert.equal(MyClass2.staticPublicField, 1);
assert.equal(MyClass2.staticPublicMethod(), 2);

16.1.3 修飾子のような名前接頭辞: #(プライベート)

class MyClass3 {
  #privateField = 1;

  #privateMethod() {
    return 2;
  }

  static accessPrivateMembers() {
    // Private members can only be accessed from inside class definitions
    const inst3 = new MyClass3();
    assert.equal(inst3.#privateField, 1);
    assert.equal(inst3.#privateMethod(), 2);
  }
}
MyClass3.accessPrivateMembers();

JavaScriptに関する警告

TypeScriptはバージョン3.8からプライベートフィールドをサポートしていますが、現在プライベートメソッドはサポートしていません。

16.1.4 アクセサーの修飾子: get(ゲッター)とset(セッター)

大まかに言うと、アクセサーはプロパティにアクセスすることによって呼び出されるメソッドです。アクセサーには、ゲッターとセッターの2種類があります。

class MyClass5 {
  #name = 'Rumpelstiltskin';
  
  /** Prototype getter */
  get name() {
    return this.#name;
  }

  /** Prototype setter */
  set name(value) {
    this.#name = value;
  }
}
const inst5 = new MyClass5();
assert.equal(inst5.name, 'Rumpelstiltskin'); // getter
inst5.name = 'Queen'; // setter
assert.equal(inst5.name, 'Queen'); // getter

16.1.5 メソッドの修飾子: *(ジェネレーター)

class MyClass6 {
  * publicPrototypeGeneratorMethod() {
    yield 'hello';
    yield 'world';
  }
}

const inst6 = new MyClass6();
assert.deepEqual(
  [...inst6.publicPrototypeGeneratorMethod()],
  ['hello', 'world']);

16.1.6 メソッドの修飾子: async

class MyClass7 {
  async publicPrototypeAsyncMethod() {
    const result = await Promise.resolve('abc');
    return result + result;
  }
}

const inst7 = new MyClass7();
inst7.publicPrototypeAsyncMethod()
  .then(result => assert.equal(result, 'abcabc'));

16.1.7 計算されたクラスメンバー名

const publicInstanceFieldKey = Symbol('publicInstanceFieldKey');
const publicPrototypeMethodKey = Symbol('publicPrototypeMethodKey');

class MyClass8 {

  [publicInstanceFieldKey] = 1;

  [publicPrototypeMethodKey]() {
    return 2;
  }
}

const inst8 = new MyClass8();
assert.equal(inst8[publicInstanceFieldKey], 1);
assert.equal(inst8[publicPrototypeMethodKey](), 2);

コメント

16.1.8 修飾子の組み合わせ

フィールド(レベルがない場合は、インスタンスレベルで構成が存在することを意味します)

レベル 可視性
(インスタンス)
(インスタンス) #
static
static #

メソッド(レベルがない場合は、プロトタイプレベルで構成が存在することを意味します)

レベル アクセサー 非同期 ジェネレーター 可視性
(プロトタイプ)
(プロトタイプ) get
(プロトタイプ) set
(プロトタイプ) async
(プロトタイプ) *
(プロトタイプ) async *
(プロトタイプ関連) #
(プロトタイプ関連) get #
(プロトタイプ関連) set #
(プロトタイプ関連) async #
(プロトタイプ関連) * #
(プロトタイプ関連) async * #
static
static get
static set
static async
static *
static async *
static #
static get #
static set #
static async #
static * #
static async * #

メソッドの制限

16.1.9 内部構造

クラスでは、プロトタイプオブジェクトのチェーンが2つあることを念頭に置いておくことが重要です。

次のプレーンなJavaScriptの例を考えてみましょう

class ClassA {
  static staticMthdA() {}
  constructor(instPropA) {
    this.instPropA = instPropA;
  }
  prototypeMthdA() {}
}
class ClassB extends ClassA {
  static staticMthdB() {}
  constructor(instPropA, instPropB) {
    super(instPropA);
    this.instPropB = instPropB;
  }
  prototypeMthdB() {}
}
const instB = new ClassB(0, 1);

1は、ClassAおよびClassBによって作成されるプロトタイプチェーンがどのようなものかを示しています。

Figure 1: The classes ClassA and ClassB create two prototype chains: One for classes (left-hand side) and one for instances (right-hand side).

16.1.10 プレーンなJavaScriptでのクラス定義に関する詳細情報

16.2 TypeScriptでの非公開データスロット

デフォルトでは、TypeScriptのすべてのデータスロットはパブリックプロパティです。データを非公開にするには、次の2つの方法があります。

次に両方を見ていきます。

TypeScriptは現在、プライベートメソッドをサポートしていないことに注意してください。

16.2.1 プライベートプロパティ

プライベートプロパティはTypeScriptのみ(静的)の機能です。任意のプロパティにキーワードprivateを付けることでプライベートにすることができます(A行)。

class PersonPrivateProperty {
  private name: string; // (A)
  constructor(name: string) {
    this.name = name;
  }
  sayHello() {
    return `Hello ${this.name}!`;
  }
}

これで、間違ったスコープでそのプロパティにアクセスすると、コンパイル時エラーが発生します(A行)。

const john = new PersonPrivateProperty('John');

assert.equal(
  john.sayHello(), 'Hello John!');

// @ts-expect-error: Property 'name' is private and only accessible
// within class 'PersonPrivateProperty'. (2341)
john.name; // (A)

ただし、privateは実行時には何も変更しません。そこでは、プロパティ.nameはパブリックプロパティと区別できません。

assert.deepEqual(
  Object.keys(john),
  ['name']);

また、クラスがコンパイルされるJavaScriptコードを見ると、プライベートプロパティが実行時に保護されていないことがわかります。

class PersonPrivateProperty {
  constructor(name) {
    this.name = name;
  }
  sayHello() {
    return `Hello ${this.name}!`;
  }
}

16.2.2 プライベートフィールド

プライベートフィールドは、TypeScriptがバージョン3.8からサポートしている新しいJavaScript機能です。

class PersonPrivateField {
  #name: string;
  constructor(name: string) {
    this.#name = name;
  }
  sayHello() {
    return `Hello ${this.#name}!`;
  }
}

このバージョンのPersonは、ほとんどの場合、プライベートプロパティバージョンと同じように使用されます。

const john = new PersonPrivateField('John');

assert.equal(
  john.sayHello(), 'Hello John!');

ただし、今回はデータが完全にカプセル化されています。クラス外でプライベートフィールド構文を使用すると、JavaScriptの構文エラーになります。そのため、このコードを実行できるように、A行でeval()を使用する必要があります。

assert.throws(
  () => eval('john.#name'), // (A)
  {
    name: 'SyntaxError',
    message: "Private field '#name' must be declared in "
      + "an enclosing class",
  });

assert.deepEqual(
  Object.keys(john),
  []);

コンパイル結果は、以前よりもはるかに複雑になりました(少し簡略化されています)。

var __classPrivateFieldSet = function (receiver, privateMap, value) {
  if (!privateMap.has(receiver)) {
    throw new TypeError(
      'attempted to set private field on non-instance');
  }
  privateMap.set(receiver, value);
  return value;
};

// Omitted: __classPrivateFieldGet

var _name = new WeakMap();
class Person {
  constructor(name) {
    // Add an entry for this instance to _name
    _name.set(this, void 0);

    // Now we can use the helper function:
    __classPrivateFieldSet(this, _name, name);
  }
  // ···
}

このコードは、インスタンスデータを非公開にするための一般的な手法を使用しています。

このトピックの詳細については、「JavaScript for impatient programmers」を参照してください。

16.2.3 プライベートプロパティとプライベートフィールドの比較

16.2.4 プロテクトプロパティ

プライベートフィールドとプライベートプロパティはサブクラスではアクセスできません(A行)。

class PrivatePerson {
  private name: string;
  constructor(name: string) {
    this.name = name;
  }
  sayHello() {
    return `Hello ${this.name}!`;
  }
}
class PrivateEmployee extends PrivatePerson {
  private company: string;
  constructor(name: string, company: string) {
    super(name);
    this.company = company;
  }
  sayHello() {
    // @ts-expect-error: Property 'name' is private and only
    // accessible within class 'PrivatePerson'. (2341)
    return `Hello ${this.name} from ${this.company}!`; // (A)
  }  
}

A行でprivateからprotectedに切り替えることで、前の例を修正できます(整合性を保つためにB行でも切り替えます)。

class ProtectedPerson {
  protected name: string; // (A)
  constructor(name: string) {
    this.name = name;
  }
  sayHello() {
    return `Hello ${this.name}!`;
  }
}
class ProtectedEmployee extends ProtectedPerson {
  protected company: string; // (B)
  constructor(name: string, company: string) {
    super(name);
    this.company = company;
  }
  sayHello() {
    return `Hello ${this.name} from ${this.company}!`; // OK
  }  
}

16.3 プライベートコンストラクター

コンストラクターもプライベートにすることができます。これは、静的ファクトリメソッドがあり、クライアントに常にそれらのメソッドを使用させ、コンストラクターを直接使用させたくない場合に役立ちます。静的メソッドはプライベートクラスメンバーにアクセスできるため、ファクトリメソッドは引き続きコンストラクターを使用できます。

次のコードでは、静的ファクトリメソッドDataContainer.create()が1つあります。これは、非同期でロードされたデータを介してインスタンスを設定します。非同期コードをファクトリメソッドに保持すると、実際のクラスを完全に同期的にすることができます。

class DataContainer {
  #data: string;
  static async create() {
    const data = await Promise.resolve('downloaded'); // (A)
    return new this(data);
  }
  private constructor(data: string) {
    this.#data = data;
  }
  getData() {
    return 'DATA: '+this.#data;
  }
}
DataContainer.create()
  .then(dc => assert.equal(
    dc.getData(), 'DATA: downloaded'));

実際のコードでは、A行で非同期的にデータをロードするためにfetch()または同様のPromiseベースのAPIを使用します。

プライベートコンストラクターは、DataContainerがサブクラス化されるのを防ぎます。サブクラスを許可する場合は、protectedにする必要があります。

16.4 インスタンスプロパティの初期化

16.4.1 厳密なプロパティの初期化

コンパイラ設定--strictPropertyInitializationがオンになっている場合(--strictを使用する場合はそうなる)、TypeScriptは宣言されたすべてのインスタンスプロパティが正しく初期化されているかどうかを確認します。

ただし、TypeScriptが認識しない方法でプロパティを初期化することがあります。その場合は、感嘆符(definite assignment assertions)を使用して、TypeScriptの警告をオフにすることができます(A行とB行)。

class Point {
  x!: number; // (A)
  y!: number; // (B)
  constructor() {
    this.initProperties();
  }
  initProperties() {
    this.x = 0;
    this.y = 0;
  }
}
16.4.1.1 例: オブジェクトを介したインスタンスプロパティの設定

次の例では、definite assignment assertionsも必要です。ここでは、コンストラクターパラメータpropsを介してインスタンスプロパティを設定します。

class CompilerError implements CompilerErrorProps { // (A)
  line!: number;
  description!: string;
  constructor(props: CompilerErrorProps) {
    Object.assign(this, props); // (B)
  }
}

// Helper interface for the parameter properties
interface CompilerErrorProps {
  line: number,
  description: string,
}

// Using the class:
const err = new CompilerError({
  line: 123,
  description: 'Unexpected token',
});

16.4.2 コンストラクターパラメータをpublicprivate、またはprotectedにする

コンストラクターパラメータにキーワードpublicを使用すると、TypeScriptは2つのことを行います。

したがって、次の2つのクラスは同等です。

class Point1 {
  constructor(public x: number, public y: number) {
  }
}

class Point2 {
  x: number;
  y: number;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

publicの代わりにprivateまたはprotectedを使用すると、対応するインスタンスプロパティはプライベートまたはプロテクトになります(パブリックではありません)。

16.5 抽象クラス

TypeScriptでは、2つの構成要素を抽象にすることができます。

次のコードは、抽象クラスと抽象メソッドを示しています。

一方では、抽象スーパークラスPrintableとそのヘルパークラスStringBuilderがあります。

class StringBuilder {
  string = '';
  add(str: string) {
    this.string += str;
  }
}
abstract class Printable {
  toString() {
    const out = new StringBuilder();
    this.print(out);
    return out.string;
  }
  abstract print(out: StringBuilder): void;
}

他方では、具象サブクラスEntriesEntryがあります。

class Entries extends Printable {
  entries: Entry[];
  constructor(entries: Entry[]) {
    super();
    this.entries = entries;
  }
  print(out: StringBuilder): void {
    for (const entry of this.entries) {
      entry.print(out);
    }
  }
}
class Entry extends Printable {
  key: string;
  value: string;
  constructor(key: string, value: string) {
    super();
    this.key = key;
    this.value = value;
  }
  print(out: StringBuilder): void {
    out.add(this.key);
    out.add(': ');
    out.add(this.value);
    out.add('\n');
  }
}

最後に、これがEntriesEntryを使用している私たちのものです。

const entries = new Entries([
  new Entry('accept-ranges', 'bytes'),
  new Entry('content-length', '6518'),
]);
assert.equal(
  entries.toString(),
  'accept-ranges: bytes\ncontent-length: 6518\n');

抽象クラスに関するメモ