【初心者向け】TypeScript超入門#04 クラス編

2020-06-28
hero画像

はじめに

おはようございます!こんにちは!こんばんは!
のふのふ(@rpf_nob)と申します!!都内のスタートアップでフロントエンドエンジニアとして働いています。

この記事は TypeScript 超入門シリーズの第 4 回目として、TypeScript のクラスについてまとめて解説していきます!

ソースコードは以下 GitHub を参照してください。

基本的なクラスの書き方

基本的には JavaScript のクラスにコンストラクタで受け取る引数とメンバー変数やメソッドの戻り値に型を指定してあげるだけです。

コンストラクタはメソッドですが、戻り値の型を指定する必要はありません。TypeScript の言語仕様です。

src/04_class-types.ts
class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  introduce(): string {
    return `私の名前は${this.name}です。年齢は${this.age}歳です。`;
  }
}

const sato = new Person("佐藤太郎", 30);
console.log(sato.name); //→佐藤太郎
console.log(sato.age);  //→30
console.log(sato.introduce()); // →私の名前は佐藤太郎です。年齢は30歳です。

後述するクラスメンバー修飾子をコンストラクタで受け取る引数に付与してあげることで、メンバー変数の初期化もしてくれます。

次のコードは上記のPersonクラスと同一のものになります。

src/04_class-types/040_basic-clas/.ts
class Person2 {
  constructor(public name: string, public age: number) {}

  introduce(): string {
    return `私の名前は${this.name}です。年齢は${this.age}歳です。`;
  }
}
const tanaka = new Person2("田中次郎", 25);
console.log(tanaka.name); //→田中次郎
console.log(tanaka.age);  //→25
console.log(tanaka.introduce()); // →私の名前は田中次郎です。年齢は25歳です。

クラスメンバー修飾子(アクセシビリティ)

基本的には他の言語と同じようにpublic・private・protected修飾子を付与することができます。

  • public→ どこからでも参照・実行が可能
  • private→ 同一クラス内のみ参照・実行が可能
  • protected→ 継承されたサブクラス内でも参照・実行が可能

次の場合はageprivateを付与しているので、ageにはPerson1 クラスからしか直接アクセスはできません。継承しているPerson2 クラスからもアクセスできません。

genderにはprotectedを付与しているので、継承しているPerson2 クラスからもアクセスできます。

修飾子を付与しない場合はpublicと同じになります。基本的にはpublicは書かないのが普通のようです。

src/04_class-types/041_member-accessibility.ts
class Person1 {
  public name: string;
  private age: number;
  protected gender: string;

  constructor(name: string, age: number, gender: string) {
    this.name = name;
    this.age = age;
    this.gender = gender;
  }

  introduce(): string {
    return `私の名前は${this.name}です。年齢は${this.age}歳です。`;
  }
}
class Person2 extends Person1 {
  constructor(name: string, age: number, gender: string) {
    super(name, age, gender);
  }

  introduce(): string {
    return `私の名前は${this.name}です。${this.age}歳の${this.gender}です。`;
    //→NG 親クラス(Person1)のageがprivateなので[age]にアクセスできない
  }
}
const tanaka = new Person1("田中次郎", 25, "男性");
console.log(tanaka.name); //→田中次郎
console.log(tanaka.age); //→NG(ageはprivateなのでアクセスできない)
console.log(tanaka.gender); //→NG(genderはprotectedなのでアクセスできない)
console.log(tanaka.introduce());  // 私の名前は田中次郎です。年齢は25歳です。

const suzuki = new Person2("鈴木花子", 20, "女性");
console.log(suzuki.name); //→鈴木花子
console.log(suzuki.age); //→NG(ageはprivateなのでアクセスできない)
console.log(suzuki.gender); //→NG(genderはprotectedなのでアクセスできない)
console.log(suzuki.introduce());  //→NG(ageがprivateなのでメソッド自体がエラー)

getter と setter

オブジェクト指向プログラミングの定石であるgetter と setterは TypeScript にもあります。getter と setter については他の言語でもいろいろと賛否があるみたいですが、言語仕様としてあるので理解はしておいたほうがいいです。

getter と setter の必要性を知りたい方はオブジェクト指向プログラムで getter/setter メソッドを使わなければならない 10 の理由が参考になると思います。

とりあえず簡単に言うと、getter と setter を使うことによって、アクセス制御ができるようにして、どこでメンバ変数の参照や書き換えをしているかをコード中で見やすくするようなことです。

それでは簡単な例を見てみましょう。

src/04_class-types/042_getter-setter.ts
class Person {
  private _name: string;
  private _age: number;

  constructor(name: string, age: number) {
    this._name = name;
    this._age = age;
  }

  get name() {
    return this._name;
  }
  set age(num: number) {
    this._age = num;
  }
}
const yamada = new Person("山田三郎", 20);

console.log(yamada._name);  //→NG(privateなので直接アクセスできない)
console.log(yamada.name); // 山田三郎

yamada._age = 30;  //→NG(privateなので直接アクセスできない)
yamada.age = 30;  //→OK

この場合、以下のようなアクセス制御になっています。

  • メンバ変数の[_name]には直接参照できないが、[getter]を通して[_name]にアクセスしている。
  • メンバ変数の[_age]は直接書き換えできないが、[setter]を通して[_age]を書き換えている。

このように、直接外部からアクセスできないようにして、[getter]や[setter]を介すことによって、アクセスの追跡をしやすくできます。

ちなみにメンバ変数と[getter]や[setter]の名称が同じになってしまう場合は、慣習としてメンバ変数の先頭に[_]をつけることが多いです。

static(静的メンバ)

staticを使用することによって、クラスのインスタンスを作成しなくてもメンバ変数やメソッドにアクセスすることができます。

次の例では直接[Person4]クラスのメンバ変数やメソッドにアクセスして使用しています。

src/04_class-types/043_static-member.ts
class Person {
  static firstName: string = "太郎";
  static lastName: string = "山田";
  static age: number = 18;

  // メソッド
  static introduce(): string {
    return `私の名前は${this.lastName}${this.firstName}です。${this.age}歳です。`;
  }
}
console.log(Person.firstName); //→太郎
console.log(Person.lastName);  //→山田
console.log(Person.age); //→18
console.log(Person.introduce()); //→私の名前は山田太郎です。18歳です。

継承

クラスの継承に関しては基本的には JavaScript と同じになります。 子クラス内の constructer でsuperメソッドを呼んであげれば、親クラスのメンバ変数を初期化できます。

また、親クラスのメソッドを子クラスでも使用できます。
次の例では Dog クラス(子クラス)で Animal クラス(親クラス)のcryメソッドをsuper.cry()で実行しています。

src/04_class-types/044_Inheritance.ts
class Animal {
  constructor(public name: string) {}
  cry(): string {
    return "鳴く";
  }
}
class Dog extends Animal {
  public say: string;
  constructor(name: string, say: string) {
    super(name);
    this.say = say;
  }
  cry(): string {
    return `${this.name}${this.say}${super.cry()}`;
  }
}
const dog = new Dog("シロ", "わんわん");
console.log(dog.cry()); //→シロはわんわんと鳴く

抽象クラス

抽象クラスは先にこういうメンバ変数やメソッドがありますよというのを宣言しておいて、そのクラスを継承した子クラス内でメンバ変数やメソッドを実装する必要があると伝えるためのものです。

次の例ではAnimalクラスにnameプロパティとcryメソッドが必要ですよと、子クラスに伝えるものです。

src/04_class-types/045_abstract-class.ts
abstract class Animal {
  abstract name: string;
  abstract cry(): string;
}

クラスを継承しただけの状態だと、次のようなエラーが出て、実装のし忘れなどを防ぐことができます。

src/04_class-types/045_abstract-class.ts
class Dog extends Animal {
}

非抽象クラス ‘Dog’ はクラス ‘Animal’ からの継承抽象メンバー ‘cry’ を実装しません。ts(2515)
非抽象クラス ‘Dog’ はクラス ‘Animal’ からの継承抽象メンバー ‘name’ を実装しません。ts(2515)

なので、次のように実装してあげます。

src/04_class-types/045_abstract-class.ts
class Dog extends Animal {
  name = "ポチ";
  cry() {
    return "わんわん";
  }
}

インターフェース

インターフェースを使用すれば、複数のクラスを継承(実際は実装)することができます。

src/04_class-types/046_interface.ts
interface Pitcher {
  pitching(): void;
}
interface Batter {
  batting(): void;
}
class TwoWay implements Pitcher, Batter {
  pitching(): void {
    console.log("ピッチング!");
  }
  batting(): void {
    console.log("バッティング!");
  }
}
const otani = new TwoWay();
otani.pitching(); //→ピッチング!

上の例でTwoWayクラス内にbattingメソッドを書き忘れていた場合には以下のようにエラーがでます。

クラス ‘TwoWay’ はインターフェイス ‘Batter’ を正しく実装していません。
プロパティ ‘batting’ は型 ‘TwoWay’ にありませんが、型’Batter’ では必須です。ts(2420)

こういうエラーが実装中に出るのが、TypeScript のメリットですね!

まとめ

今回は TypeScript のクラスについて解説を行いました。

クラスについては JavaScript にはない概念があったりするので、少し難しいですが、実際にコードを書いてみるとエラーが出るタイミングとか理由が何となく理解できると思います!!

クラスはオブジェクト指向プログラミングの柱みたいなものなので、しっかり理解したいですね!!

次回はジェネリクスについてまとめていきたいと思います。



最後まで見ていただきありがとうございました!! この記事が良かったと思ったら SHARE していただけると泣いて喜びます 🤣


©2020-2023.のふのふ🀄All Rights Reserved.