クラスと継承

公開:2020/09/28
更新:2020/09/28
10 min読了の目安(約6500字TECH技術記事

はじめに

JavaScriptでも馴染み深いクラスについてTypescriptでの記法を確認していきます。

Typescriptによるクラス宣言

class Person {
  name: string; //this.nameの型指定
  age: number; //this.ageの型指定
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  hello() {
    console.log(`hello ${this.name}`);
  }
}
const bob = new Person("Bob", 22); //生成されるインスタンスはオブジェクト
bob.hello(); //hello Bob

コンストラクター関数の引数にあるような引数の型指定は、一般的な関数同様です。
クラスならではの型指定は、今回の例でいうとclassとconstoructorに挟まれたname:stringです。クラスではletやconstで変数名を宣言できないため、その代わりにthis.をプロパティ名の前に付けます。このthis+プロパティ名はクラスのメソッド内で多用されます。そのプロパティ名(name)と引数のnameをコンストラクター関数内で紐付けています。(ageも同様です。)

ここで一つ問題があります。
以下のように、クラス内の変数名をクラス外で書き換え可能の状態です。

const bob = new Person("Bob", 22); //生成されるインスタンスはオブジェクト
bob.name = 'Green'//変数名nameを書き換え
bob.hello(); //hello Green

アクセス修飾子

先程の問題のコードを改善するためにTypeScriptにはアクセス修飾子privateというものがあります。

class Person {
  private name: string; //アクセス修飾子private
  age: number; //デフォルトではpublic(このように省略可)
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  hello() {
    console.log(`hello ${this.name}`);
  }
}
const bob = new Person("Bob", 22); //生成されるインスタンスはオブジェクト
bob.name = "Green"; //エラー!(プロパティ 'name' はプライベートで、クラス 'Person' 内でのみアクセスできます。)
bob.hello(); //hello Green(Typescriptは静的な言語なのでコンソール上ではhello Greenが生成されます。)

プロパティ名の前にprivateと付けることでそのプロパティはクラス外からアクセスをシャットダウンする事が可能となります。

またprivateを#で表現することも可能です。#をprivate識別子と言います。

class Person {
  #name: string; //アクセス修飾子private
  age: number; //デフォルトではpublic(このように省略可)
  constructor(name: string, age: number) {
    this.#name = name;
    this.age = age;
  }
  hello() {
    console.log(`hello ${this.#name}`);
  }
}
const bob = new Person("Bob", 22); //生成されるインスタンスはオブジェクト
bob.hello(); //hello Green(Typescriptは静的な言語なのでコンソール上ではhello Greenが生成されます。)

クラスの省略記法

大体の場合プロジェクトにはたくさんのクラスが存在し、そのクラスの内部にはいくつかのプロパティが存在します。それらには初期値を設定しなければならないので、毎回プロパティ名と引数を紐付けるのは面倒です。
そこでこの面倒事を改善してくれる省略記法があります。
先程のアクセス修飾子を導入したコードを省略記法で表してみます。

class Person {
  constructor(private name: string, public age: number) {}
  hello() {
    console.log(`hello ${this.name}`);
  }
}
const bob = new Person("Bob", 22); //生成されるインスタンスはオブジェクト

このように簡潔にクラス宣言を記述できます。
注意すべきは、アクセス修飾子は変数名の前に必ずつけなければならないということです。(たとえデフォルトのpublicでも)

またコンストラクタ関数の引数には、private識別子#を利用できません。
app.ts


readonly

先程クラス外で書き換え不可にするためのprivate修飾子を導入しましたが、クラス内では書き換えられます。

class Person {
  constructor(private name: string, public age: number) {}
  hello() {
    this.name = "Green";//nameプロパティを書き換えられてしまう。
    console.log(`hello ${this.name}`);
  }
}
const bob = new Person("Bob", 22); //生成されるインスタンスはオブジェクト
bob.hello();//hello Green

クラス内でもプロパティの値を変更不可(const宣言のように)にするためにreadonly修飾子を使って、クラスフィールドを読み取り専用と指定することです。

class Person {
  constructor(private readonly name: string, public age: number) {}
  hello() {
    this.name = "Green"; //エラー(読み取り専用プロパティであるため、'name' に代入することはできません。)
    console.log(`hello ${this.name}`);
  }
}
const bob = new Person("Bob", 22); //生成されるインスタンスはオブジェクト

クラス継承

早速ですがコードで確認しましょう。

class Person {
  constructor(private readonly name: string, public age: number) {}
  hello() {
    console.log(`hello ${this.name}`);
  }
}
const bob = new Person("Bob", 22); //生成されるインスタンスはオブジェクト

class Japanese extends Person {
  //継承先 extends 継承元
  constructor(name: string, age: number, private gender: "male" | "female") {
    super(name, age); //superメソッドで継承元のプロパティを継承先でも使える状態にします。
  }
  hello() {
    //←継承元のメソッドを上書きすることができます。
    console.log("konnichiwa" + this.name);
  }
}
const takashi = new Japanese("Takashi", 24, "male");
takashi.hello(); //konnichiwaTakashi

コードについて、軽く解説を入れます。
JapaneseクラスがPerson クラスを継承しているという関係です。
また、Japaneseクラスのプロパティの一つにgenderがあります。この型は'male'|'female'、つまりリテラル型とユニオン型の複合となります。genderの値はmaleかfemaleどちらかしか受け付けないということを意味します。

上記のコードですが、現状tsファイルではエラーが出ています。
app.ts
このエラーの原因はnameプロパティにprivate修飾子をつけていることにあります。

private修飾子をつけたプロパティはそのクラス(Personクラス)からのみアクセスできるというものでした。それをJapaneseクラスで使おうとしているためエラーが発生しています。


protected修飾子

直前のエラーを解消するためには、2つの改善策があります。

①単にprivate修飾子をなくす。

class Person {
  constructor(readonly name: string, public age: number) {}
  hello() {
    console.log(`hello ${this.name}`);
  }
}
const bob = new Person("Bob", 22); //生成されるインスタンスはオブジェクト

class Japanese extends Person {
  //継承先 extends 継承元
  constructor(name: string, age: number, private gender: "male" | "female") {
    super(name, age); //superメソッドで継承元のプロパティを継承先でも使える状態にします。
  }
  hello() {
    //←継承元のメソッドを上書きすることができます。
    console.log("konnichiwa" + this.name);
  }
}
const takashi = new Japanese("Takashi", 24, "male");
takashi.hello(); //konnichiwaTakashi

privateをなくすだけだと、アクセス修飾子はpublic修飾子扱いとなり、readonlyがついているため書き換えは不可ですが、それでもクラス外からアクセス可能の筒抜けのクラスになっています。

②privateの代わりにアクセス修飾子の一つであるprotected修飾子を使う。

class Person {
  constructor(protected readonly name: string, public age: number) {}
  hello() {
    console.log(`hello ${this.name}`);
  }
}
const bob = new Person("Bob", 22); //生成されるインスタンスはオブジェクト
console.log(bob.name); //Bob//エラー(プロパティ 'name' は保護されているため、クラス 'Person' とそのサブクラス内でのみアクセスできます。)

class Japanese extends Person {
  //継承先 extends 継承元
  constructor(name: string, age: number, private gender: "male" | "female") {
    super(name, age); //superメソッドで継承元のプロパティを継承先でも使える状態にします。
    this.gender = gender; //継承元のプロパティを受け継いだあとにJapanese独自のプロパティを設定。
  }
  hello() {
    console.log("konnichiwa" + this.name); //←継承元のメソッドを上書きすることができます。
  }
}

const takashi = new Japanese("Takashi", 24, "male");
takashi.hello(); //konnichiwaTakashi

①の欠点を補うため、今回のケースではprotected修飾子を使うことでクラス外からのアクセスを制御し、また継承先(Japaneseクラス)でnameプロパティを使えるというコードに修正できました。


static修飾子

classの修飾子の一つにstaticがあります。newをしてインスタンス化せずに直接クラスにアクセスすることでメソッドやプロパティを使える修飾子です。因みに、staticは静的と訳されて、静的メッソド静的プロパティとも呼ばれます。
ここではPersonクラスをいじり、staticの使い方を見てみましょう。

class Person {
  static birthdate = "1998/01/01";
  static legLength(leg: number) {
    return { length: leg };
  }
  constructor(protected readonly name: string, public age: number) {}
  hello() {
    console.log(`hello ${this.name}`);
  }
}
console.log(Person.birthdate);//1998/01/01
console.log(Person.legLength(27));//{length: 27}

このようにインスタンス化せずにクラス内にアクセス可能となります。