TypeScriptのクラス構文について調べました【タイプスクリプト/JavaScript/Class】

JavaScript

なんとなく使っていたクラス構文について調べたのでメモ。
今後、需要があって使いそうなTypeScriptについてまとめています。

クラス構文とは

クラス構文とは、関連するデータとメソッドをまとめるための仕組みで、最終的にはこのクラスをベースにオブジェクトを生成します。

クラス構文を使わず、オブジェクトだけで同じことを実装することは可能ですが、データが煩雑になり、分かりにくくなるのが欠点です。

クラス構文の特長として、同じクラスから作成されたオブジェクトは、同じプロパティとメソッドを持ちます。なお、クラスを元にオブジェクトを作成する事をインスタンス化と呼びます。

参考サイト

クラス - JavaScript | MDN
クラスはオブジェクトを作成するためのテンプレートです。処理するためのコードでデータをカプセル化します。 JS のクラスはプロトタイプに基づいて構築されていますが、一部の構文や意味はクラスに固有です。

基本的な使い方

こちらが基本的な使い方です。

// クラス名の頭文字は大文字
class MyClass {
  // クラスフィールドで型を設定する
  propertyString: string;
  // クラスフィールドで型と初期値を設定する
  propertyNumber: number = 0;

  // 初期化(インスタンスが作成された時に実行)
  constructor(p: string) {
    // メンバ変数に引数の値を設定
    this.propertyString = p;
  }
  // メソッド(thisの型を指定する)
  method(this:MyClass) {
    console.log(`propertyString:${this.propertyString}`);
  }
}
// インスタンス化
const instance = new MyClass('値その1');
instance.method(); //propertyString:値その1コンソールに出力
実行結果

propertyString:値その1

インスタンス化するときは、次のようにクラス名にnewをつけます。
基本的に変数に格納して使用します。

// インスタンス化
const instance = new MyClass('値その1');
instance.method(); //propertyString:値その1コンソールに出力

クラス宣言

クラス構文は、次のように冒頭にclassをつけてオブジェクトのように書きます。(=は必要なし)
お作法としてクラス名の頭文字は大文字にする必要があります。

// クラス名の頭文字は大文字
class MyClass {}

クラスフィールド

クラスフィールドは、クラス内で定義する変数です。定義位置は自由ですが、主にクラスの先頭に追加します。ここに変数名と型を定義し、クラスが持つ変数の型を明示的に指定することができます。
また、クラスフィールドで初期値を設定できます。

クラスの要素全てに言えることですが、クラス内で要素にアクセスするには、this.<変数名>でアクセスすることができます。

class MyClass {
  // クラスフィールドで型を設定する
  propertyString: string = "初期値";
  propertyNumber: number = 0;
}
// インスタンス化
const instance = new MyClass();
instance.propertyString; // 初期値
instance.propertyNumber;// 0

メンバ変数、メンバ関数

クラス内の要素をメンバと呼びます。メンバ変数は後述するコンストラクタで初期化される変数で、メンバ関数(メソッド)は、クラス内で定義された関数です。

class MyClass {
  propertyString: string;

  constructor(p: string) {
    this.propertyString = p; // thisでメンバ変数にアクセス
  }
  // メソッド(thisの型を指定する)
  method(this:MyClass) {
    console.log(`propertyString:${this.propertyString}`);
  }
}

尚、メンバ関数内のthisを自身のクラスだと明示的に指定する時は、引数にthis:<クラス名>を指定します。

// thisの型はMyClassであることを明示的に指定。
method(this:MyClass) {
    console.log(`propertyString:${this.propertyString}`);
  }

コンストラクタメソッド

コンストラクタ関数は、クラスから新しいオブジェクトを作成(インスタンス化)する際に実行される特別な関数であり、オブジェクトを初期化します。

クラスフィールドで型だけ定義した変数の初期値を、コンストラクタ内で初期化することもできます。以下コードの場合、インスタンス化の際に受け取った引数「値その1」をオブジェクトのプロパティ「this.propertyString」に格納しています。

class MyClass {
  // クラスフィールドで型を設定する
  propertyString: string; // 型だけ定義
  propertyNumber: number = 0;

  // 初期化(インスタンスが作成された時に実行)
  constructor(p: string) {
    // メンバ変数に引数の値を設定
    this.propertyString = p; // 値その1
  }
}

// インスタンス化
const instance = new MyClass('値その1');
instance.propertyString; // 値その1
instance.propertyNumber; // 0

アクセス修飾子(private、public、readonly)

アクセス修飾子はメンバの前に指定することで、そのメンバがどこからアクセス可能かを設定します。
主にクラス外からのアクセスか可能か?書き換えは可能か?を指定できます。

  • public:クラス内外関係なくアクセス可能(デフォルト値)
  • private:クラス内でアクセス可能。クラスの外からのアクセス不可
  • readonly:クラス内外関係なく、初期化後の値が変更できないことを明示的に設定する

以下コードのように、メンバの前に記述します。
なお、publicは初期値なので省略可能です。

class MyClass  {
  // パブリック(クラスの外からでもアクセス可能:デフォルト)
  public publicProperty: string = "";
  // プライベート(クラスの外からアクセス不可)
  private privateProperty: string ="";
   // 読み取り専用(初期化後の値が変更できないことを明示的に設定する)
  readonly readonlyPropaty: string = "";
}

サンプルコード

public、private、readonlyの挙動の違いを以下のコードにまとめました。
エラーになる箇所はコメントアウトしています。

// クラス名の頭文字は大文字
class ClassName {
  // プライベートプロパティ
  private privateProperty: string;
  // パブリックプロパティ
  public publicProperty: string;
  // 読み取り専用
  readonly readonlyPropaty: string;

  // 初期化(インスタンスが作成された時に実行)
  constructor(pri: string, pub: string, rea: string) {
    // プロパティに引数の値を設定
    this.privateProperty = pri;
    this.publicProperty = pub;
    this.readonlyPropaty = rea;
  }
  method() {
    // クラス内で書き換え
    this.privateProperty = 'クラス内で書換え可能'; // ok!
    this.publicProperty = 'クラス内外関係なく書き換え可能'; // ok!
    // this.readonlyPropaty = 'クラス内外関係なく書き換え不可'; // error!

    // クラス内でアクセス
    console.log('privateProperty:' + this.privateProperty); // ok!
    console.log('publicProperty:' + this.publicProperty); // ok!
    console.log('readonlyPropaty:' + this.readonlyPropaty); // ok!
  }
}
// インスタンス化
const instance = new ClassName('プライベート値', 'パブリック値', '読み取り専用値');

// クラス外で書き換え
// instance.privateProperty = 'クラス外の書き換え不可'; // error!
instance.publicProperty = 'クラス内外関係なく書き換え可能'; // ok!
// instance.readonlyPropaty = 'クラス内外関係なく書き換え不可'; // error!

// クラス外でアクセス
// console.log("privateProperty:" + instance.privateProperty); // error!
console.log('publicProperty:' + instance.publicProperty); // ok!
console.log('readonlyPropaty:' + instance.readonlyPropaty); // ok!

メンバ初期化時のショートカット構文

コンストラクト関数でオブジェクトを初期化する際、ショートカット構文で短く記述することができます。例えば、次のようなクラスフィールドとコンストラクタ関数があるとします。

通常通りの書き方

class MyClass {
  // プライベートプロパティ
  private privateProperty: string;
  // パブリックプロパティ
  public publicProperty: string;
  // 読み取り専用
  readonly readonlyPropaty: string;

  // 初期化(インスタンスが作成された時に実行)
  constructor(pri: string, pub: string, rea: string) {
    // プロパティに引数の値を設定
    this.privateProperty = pri;
    this.publicProperty = pub;
    this.readonlyPropaty = rea;
  }
} 

ショートカット構文

こちらがショートカット構文です。
引数と同じ名前でメンバを初期化してくれます。初期値として引数として渡された値が格納されます。
尚、ショートカット構文の場合、public修飾子は省略できません。

class ClassName {
  // 引数をメンバの初期値として設定する
  constructor(
    private privateProperty: string,
    public publicProperty: string,
    readonly readonlyPropaty: string
  ) {
    // 記述なし
  }
}

継承(extends)

継承はクラスを拡張したい時に使います。
基本的な使い方は次のようになります。

// 継承元クラス
class BaseClass {
  constructor(protected id: string, private baseProp:string) {}
  printBase() {
    console.log(`BaseClassのメソッド実行 id:${this.id},basePropの値${this.baseProp}`);
  }
}

// extendsに継承したいクラス名を指定
class ExtensionClass extends BaseClass {
  extensionUniquePorp:string;
  constructor(id: string, extensionUniquePorp:string) {
    // 継承元のconstructorを呼び出す
    super(id, "ベースプロパティ");
    // 継承先独自のプロパティはsuper()の後に記述
    this.extensionUniquePorp = extensionUniquePorp;
  }
  // 継承元メソッド上書き
  printBase() {
    // 継承元のidプロパティはprotectedなので継承したサブクラスでプロパティの上書きが可能
    this.id = "継承先で値書き換え";

    console.log(`BaseClassのメソッド上書き protected修飾子のidの値を書き換えid:${this.id}`);
  }
  // 継承先独自メソッド
  printExtension() {
    console.log(`ExtensionClassのメソッド実行 id:${this.id},extensionUniquePorp:${this.extensionUniquePorp}`);
  }
}
// インスタンス化
console.log("▼▼▼ベースクラスで実行▼▼▼");
const baseClass = new BaseClass("No.1", "ベースプロパティ");
baseClass.printBase();

console.log("▼▼▼サブクラスで実行▼▼▼")
const extensionClass = new ExtensionClass("No.1-1", "エクステンドプロパティ");
extensionClass.printExtension();
extensionClass.printBase();

extendsでクラスを継承する

例えば、次のようなBaseClassがあるとします。

// 継承元クラス
class BaseClass {
  constructor(public id: string, private basicProp:string) {}
  printBase() {
    console.log(`BaseClassのメソッド実行 basicPropの値${this.basicProp}`);
  }
}

このBaseClassの機能を継承したクラスを作りたい場合、継承したいクラス名をextendsキーワードで繋ぎます。

以下のコードは、ExtensionClassBaseClassを継承しています。

// extendsに継承したいクラス名を指定
class ExtensionClass extends BaseClass {
  
}
// インスタンス化
// 継承元のクラスに2つの引数が設定されているので、2つの引数が必要
const extensionClass = new ExtensionClass("No.1", "ベーシックプロパティ");

継承したサブクラスには、継承元と同じ機能が使われるので、継承元と同じ引数が必要になります。

サブクラス独自のメンバを設定する

サブクラスに独自のメンバを追加したい時は、継承元のコンストラクタを呼び出した後に設定します。

super()とは

何も設定しなくてもサブクラスで継承元のコンストラクタは実行されますが、super()を使うと、継承元のコンストラクタを明示的に呼び出すことができます。継承元のコンストラクタで必要な引数は、super()で改めて設定します。

// 継承元クラス
class BaseClass {
  constructor(public id: string, private baseProp:string) {}
  printBase() {
    console.log(`BaseClassのメソッド実行 basePropの値${this.baseProp}`);
  }
}

// 継承先クラス 
class ExtensionClass extends BaseClass {
  constructor() {
    // 継承元のconstructorを呼び出す
    super("ID", "ベースプロパティ");
  }
}

次のようにサブクラスで継承元のコンストラクタをsuper()で呼び出します。
その後に独自のメンバを追加します。

class ExtensionClass extends BaseClass {
  extensionUniquePorp:string;
  constructor(extensionUniquePorp:string) {
    // 継承元のconstructorを呼び出す
    super("ID", "ベースプロパティ");
    // サブクラス独自のプロパティはsuper()の後に記述
    this.extensionUniquePorp = extensionUniquePorp;
  }
}
// インスタンス化
const extensionClass = new ExtensionClass("エクステンドプロパティ");

オーバーライド(上書き)

継承元で使われているメンバをサブクラス側で同じ名前にすると、上書き(オーバーライド)することができます。

以下は継承元のprintBase() を継承先でのサブクラスでオーバーライドしている例です。

// 継承元クラス
class BaseClass {
  constructor(protected id: string, private baseProp:string) {}
  printBase() {
    console.log(`BaseClassのメソッド実行 baseProp:${this.baseProp}`);
  }
}

// extendsに継承したいクラス名を指定
class ExtensionClass extends BaseClass {
  extensionUniquePorp:string;
  constructor(extensionUniquePorp:string) {
    // ベース元のconstructorを呼び出す
    super('ID', "extendsで設定");
    // 継承先独自のプロパティはsuper()の後に記述
    this.extensionUniquePorp = extensionUniquePorp;
  }
  // 継承元メソッド上書き
  printBase() {
    console.log(`extendsで上書きしたBaseClassのメソッド           extensionUniquePorp:${this.extensionUniquePorp}`);
  }
}

// インスタンス化
console.log("▼▼▼ベースクラスで実行▼▼▼");
const baseClass = new BaseClass("No.1", "ベースプロパティ");
baseClass.printBase(); // BaseClassのメソッド実行

console.log("▼▼▼サブクラスで実行▼▼▼");
const extensionClass = new ExtensionClass("エクステンドプロパティ");
extensionClass.printBase(); // extendsで上書きしたBaseClassのメソッド
実行結果

▼▼▼ベースクラスで実行▼▼▼
BaseClassのメソッド実行 baseProp:ベースプロパティ
▼▼▼サブクラスで実行▼▼▼
extendsで上書きしたBaseClassのメソッド extensionUniquePorp:エクステンドプロパティ

アクセス修飾子(protected)

private修飾子だと継承先であるサブクラスからのアクセスも拒否されますが、protected修飾子は継承したクラスからのアクセスを許可します。
以下コードのクラスでは変数のidがprotectedになります。

// 継承元クラス
class BaseClass {
  // protected修飾子でサブクラスでアクセス可能
  constructor(protected id: string) {}
  printBase() {
    console.log(`this.id:${this.id}`);
  }
}

// extendsに継承したいクラス名を指定
class ExtensionClass extends BaseClass {
  printExtension() {
    // 継承元のidプロパティはprotectedなので継承したサブクラスでプロパティの上書きが可能
    this.id = "継承先で値書き換え";
    console.log(`this.id:${this.id}`);
  }
}

// インスタンス化
console.log("▼▼▼ベースクラスで実行▼▼▼");
const baseClass = new BaseClass("No.1");
baseClass.printBase(); // this.id:No.1

console.log("▼▼▼サブクラスで実行▼▼▼");
const extensionClass = new ExtensionClass("No.1-1");
extensionClass.printExtension(); // this.id:継承先で値書き換え

サブクラスのみアクセス可能なので、インスタンスから変数idにアクセスしようとするとエラーになります。以下のconsole.log()は、エラーになります。

const extensionClass = new ExtensionClass("No.1-1");
console.log(extensionClass.id) // error

エラー内容は「プロパティ ‘id’ は保護されているため、クラス ‘BaseClass’ とそのサブクラス内でのみアクセスできます。」と表示されて、インスタンスからprotected修飾子のメンバにアクセスできないことが確認できます。

ゲッターとセッター(getter/setter)

privateメンバの値を外部から参照したり、変更したい時は、getter/setterを使います。
主な違いは次の通りです。

  • getterはクラスの値を外部から取得できます。
  • setterはクラスの値を外部から変更できます。

尚、getter/setterの値は、数値または文字列になります。
クラス内でget関数、set関数を使用することで実装できます。

ゲッター - JavaScript | MDN
get 構文は、オブジェクトのプロパティを関数に結びつけ、プロパティが参照された時に関数が呼び出されるようにします。

こちらが基本的な使い方です。

class MyClass {
  // 初期化
  constructor(private myPropStr:string) {
    this.myPropStr = myPropStr;
  }
  // getter
  get myProps() {
    console.log("ゲッターを実行しました");
    if(this.myPropStr){
       // プロパティの値を取得して返す
      return this.myPropStr;
    }
    throw new Error('値がありません');
  }
  // setter
  set myProps(value:string) {
    console.log("セッターを実行しました");
    const prevValue = this.myPropStr;
    if(!value) {
      throw new Error('値を入力してください');
    }
    this.myPropStr = `${prevValue}を${value}`;
    console.log(this.myPropStr);
  }
}

const myInstance = new MyClass("「myPropStr」の値");
// プライベートな値にクラス外からアクセス
const myNewProp = myInstance.myProps;
console.log(myNewProp + "をゲッターで取得");

// プライベートな値にクラス外から書き換え
myInstance.myProps = "セッターで書き換えました。";

ゲッター(getter)

ゲッターはクラスの値を外部から取得します。
本来クラスの外からアクセスできないprivateメンバに対して、getter経由でアクセスすることができます。get関数は、アクセスする値をreturnで返す必要があります。

class MyClass {
  // 初期化
  constructor(private myPropStr:string) {
    this.myPropStr = myPropStr;
  }
  // getterでプライベートな値を返す。
  get myProps() {
    return this.myPropStr; // 必ずreturnで値を返す。
  }
}

const myInstance = new MyClass("「myPropStr」の値");
// プライベートな値にクラス外からアクセス
const myNewProp = myInstance.myProps; // プロパティのようにアクセスできる
console.log(myNewProp + "をゲッターで取得");

get関数で条件文を記述することで、条件によって異なる値を返すことができます。
以下は、this.myPropStrの値がない場合、エラーを返すようにしています。

  // getter
  get myProps() {
    console.log("ゲッターを実行しました");
    if(this.myPropStr){
       // プロパティの値を取得して返す
      return this.myPropStr;
    }
    throw new Error('値がありません');
  }

セッター(setter)

setterはクラスの値を外部から変更できます。
本来クラスの外からアクセスできないprivateメンバに対して、setter経由で書き換えることができます。set関数には、書き換える値として引数が必ず必要になります。

class MyClass {
  // 初期化
  constructor(private myPropStr:string) {
    this.myPropStr = myPropStr;
  }
    // setterでプライベートな値を書き換える
  set myProps(value:string) {
    this.myPropStr = value;
    console.log(this.myPropStr);
  }
}

const myInstance = new MyClass("「myPropStr」の値");

// プライベートな値にクラス外から書き換え
myInstance.myProps = "セッターで書き換えました。";

set関数で条件文を記述することで、条件によって異なる値で書き換えることができます。
以下は、this.myPropStrの値がない場合、エラーを返すようにしています。

  // setter
  set myProps(value:string) {
    console.log("セッターを実行しました");
    const prevValue = this.myPropStr;
    if(!value) {
      throw new Error('値を入力してください');
    }
    this.myPropStr = `${prevValue}を${value}`;
    console.log(this.myPropStr);
  }

staticプロパティ、staticメソッド

staticとは、日本語で「静的」という意味です。
言葉通り、staticプロパティ、staticメソッドは、インスタンス化せずに直接クラスからアクセスできます。インスタンスによって値が変わることはありません。
以下のコードは基本的な使い方となります。

class Counter {
  // staticプロパティ
  static count = 0;
  // staticプロパティ
  static increment() {
    // クラス名を使ってアクセス
    Counter.count++;
  }
  method() {
    // this.を使ってstaticプロパティにアクセス不可
    // this.count = 1;
  }
}

// staticプロパティを表示・実行
console.log(Counter.count); // 0
Counter.increment();
Counter.increment();
Counter.increment();
console.log(Counter.count); // 3

const instance = new Counter();
// instance.increment(); // error! インスタンスからはアクセスできない

staticプロパティには、インスタンスを作成することなく、クラス名で直接アクセスします。
また、更新した値はクラス内で保持されます。

staticプロパティ、staticメソッドは、クラスに関連するデータや処理を保持するために使用されます。クラス全体で共有される値やメソッドに使用します。

まとめ

今回改めてクラス構文について調べました。
TypeScriptが絡むと余計に難しいです、、。書き出すと色々な機能があるのだと思いました。

大体はオブジェクトで実装できるので、あまり使うことはないと思いますが、忘れたらこの記事を見返したいと思います!