最高にトゥースなTypeScript class編

この記事は [**セゾン情報システムズ Advent Calendar 2020**](https://qiita.com/advent-calendar/2020/sisco) 10日目の記事です。

## まえがき
TypeScriptにおける**クラス**の使い方や機能について書き記しています。

## クラスとは
ES2015より追加された機能です。
class構文機能が追加され、JavaScriptでもオブジェクト指向のアプローチがしやすくなりました。
クラスの特徴を大雑把に挙げると・・・

- クラスはオブジェクトの設計図
- クラスから作られたオブジェクトはインスタンスと呼ばれる
- 似たようなオブジェクトを複製するときに便利

といったものが挙げられます。

## クラスを定義してオブジェクトを作成
まずはクラスを定義し、インスタンス化します。
nameプロパティ（フィールド）とコンストラクタ関数の引数initNameに`string型`を宣言します。
※コンストラクタ関数はオブジェクトを作成されるときに実行される関数です。

```TypeScript:index_1.ts
class Hinatazaka {
  name: string;
  constructor(initName: string) {
    this.name = initName;
  }
}

const matsuda = new Hinatazaka('松田好花');
console.log(matsuda); // Hinatazaka: {"name": "松田好花"} 
```

## クラスにメソッドを追加する
index_1.tsにメソッドを追加します。

```TypeScript:index_2-1.ts
class Hinatazaka {
  name: string;
  constructor(initName: string) {
    this.name = initName;
  }

  // メソッド
  greeting() {
    console.log(`日向坂46の${this.name}です！`);
  }
}

const matsuda = new Hinatazaka('松田好花');
matsuda.greeting(); // 日向坂46の松田好花です！
```

さらに下記のオブジェクトを追加し、greetingメソッドを実行します。

```TypeScript
const kageyama = {
  anotherGreeting: matsuda.greeting // (property) anotherGreeting: () => void
}

kageyama.anotherGreeting(); //日向坂46のundefinedです！
```
名前の部分が`udefined`になってしまいましたが、これは`this.name`が原因です。
オブジェクトの中のメソッドで`this`を使うと、その`this`はオブジェクト自身を表します。
つまり「kageyamaオブジェクト内に`this`が未定義です！」という状態です。
これを解決するにはkageyamaオブジェクト内にもnameプロパティが必要になります。
加えてこのような間接的な形でメソッドを実行するとエラーは出ないですが、`this`が何を定義しているのかTypeScriptは推測できません。
よって`this`を型定義して明確にしてあげる必要があります。

### thisに型定義する

```TypeScript:index_2-2.ts
class Hinatazaka {
  name: string;
  constructor(initName: string) {
    this.name = initName;
  }

  greeting(this: {name: string}) { // 引数にthisの型定義を設定
    console.log(`日向坂46の${this.name}です！`);
  }
}

const matsuda = new Hinatazaka('松田好花');
matsuda.greeting(); // 日向坂46の松田好花です！

const kageyama = {
  name: '影山優佳', // nameプロパティを定義
  anotherGreeting: matsuda.greeting
}

kageyama.anotherGreeting(); // 日向坂46の影山優佳です！
```
greetingメソッドの引数に`this: {name: string}`という型定義とkageyamaオブジェクト内にnameプロパティを追加しました。
これで求める挨拶が返ってきます。

```TypeScript:anotherGreetingの型
(property) anotherGreeting: (this: {
    name: string;
}) => void
```
anotherGreetingの型は上記の通りになっており、`this`が型定義されているのが分かります。
**このように`this`をインスタンスで使用するには必要な場所（この例ではメソッド）で`this`を型定義する必要があります。**

## クラスを型として使う
TypeScriptにおけるクラスはオブジェクトの設計図という役割に加えて、**そのクラスが作り出したインスタンスを表す型も同時に作っています**。
よってクラスを型として使うことができます。
2のindex_2-2.tsに変更を加えて見ていきます。まず、greetingメソッドの引数の型定義を変更します。

```typescript
  greeting(this: Hinatazaka) { // thisの型定義をHinatazakaに変更
    console.log(`日向坂46の${this.name}です！`);
  }
```
クラス、インスタンス内で`this`を使う場合、それはクラス、インスタンス自身を表すのでクラスを型定義します。
これによって`this`は**nameプロパティとgretingメソッドを持つオブジェクト**という、より厳密な型定義になります。
このことから下記の実行部分でエラーが起こる理由が分かります。

```typescript
const kageyama = {
  name: '影山優佳',
  anotherGreeting: matsuda.greeting
}

kageyama.anotherGreeting() // ここでエラー; 
```
これはkageyamaオブジェクトにgreetingメソッドが無いことが原因です。
anotherGreetingをgreetingメソッドに変更します。

```typescript:index_３.ts
class Hinatazaka {
  name: string;
  constructor(initName: string) {
    this.name = initName;
  }

  greeting(this: Hinatazaka) { // thisの型定義をHinatazakaに変更
    console.log(`日向坂46の${this.name}です！`);
  }
}

const matsuda = new Hinatazaka('松田好花');
matsuda.greeting(); // 日向坂46の松田好花です！

const kageyama = {
  name: '影山優佳',
  greeting: matsuda.greeting // greetingメソッドに変更
}

kageyama.greeting(); // 日向坂46の影山優佳です！
```
これでクラスを型定義に使いつつ無事に実行できます。
クラスを型定義に使うことによって、より厳密・安全になると共にドキュメント的な役割を果たすメリットもあります。

## public、readonly、private修飾子
特定の修飾子を使うことによってアクセスを制限することができます。

### public修飾子
デフォルトの設定になります。何も記述が無い場合は`public`が適応されます。

### readonly修飾子
`readonly`を使うとそのクラスの中と外での読み込み専用になり、書き込みができなくなります。

```typescript
class Hinatazaka {
  readonly name: string; // readonlyを宣言
  constructor(initName: string) {
    this.name = initName;
  }
}

const matsuda = new Hinatazaka('松田好花');
matsuda.name = '影山優佳'; // Cannot assign to 'name' because it is a read-only property.
```

ただし、`readonly`はコンストラクタ関数の中だと書き換え可能になります。

```typescript
class Hinatazaka {
  readonly name: string = '松田好花'; // readonlyを宣言
  constructor() {
    this.name = '影山優佳'; // 書き換え処理
  }
}

new Hinatazaka().name; // 影山優佳
```

これはコンストラクタ関数がが初期化の処理を行うことに起因します。

### private修飾子
`private`を使うとそのクラスの中でのみ読み書き可能になり、クラスの外から読み書きできなくなります。

```typescript
class Hinatazaka {
  private name: string; // privateを宣言
  constructor(initName: string) {
    this.name = initName;
  }
}

const matsuda = new Hinatazaka('松田好花');
matsuda.name; // Property 'name' is private and only accessible within class 'Hinatazaka'.
```
### 初期化の処理を省略する
フィールドでプロパティを宣言しコンストラクタ関数内で初期化する記述は、コンストラクタ関数の引数で`public・readonly・private修飾子`のいずれかを使いプロパティを定義することによって初期化の処理を省略することができます。

```typescript
class Hinatazaka {
  constructor(public name: string) {} // 引数にpublicを宣言し、プロパティを定義
}

const matsuda = new Hinatazaka('松田好花');
matsuda.name; // 松田好花
```
フィールドやコンストラクタ関数内の記述が必要なくなるので、すっきりし可読性が良くなります。

## extends
`extends`はあるクラスの定義を他のクラスに継承するために使います。
使い方などはES2015のそれと同じです。TypeScriptだとそこに型定義が加わるイメージです。

`````typescript:index_4-1.ts
class Hinatazaka {
  constructor(public name: string, public catchphrase: string) {}

  greeting(this: Hinatazaka) {
    console.log(`${this.catchphrase} 日向坂46の${this.name}です！`);
  }
}

class Member extends Hinatazaka { // extendsを使用しHinatazakaクラスを継承
  constructor(name: string, catchphrase: string, public from: string) { // fromプロパティを追加
    super(name, catchphrase); // super関数を使い継承元のプロパティを定義
  }

  greeting() { // メソッドの上書き
    console.log(`${this.catchphrase} ${this.from}出身、日向坂46の${this.name}です！`);
  }
}

const matsuda = new Member('松田好花', '納豆大好き', '京都府');
matsuda.greeting(); // 納豆大好き 京都府出身、日向坂46の松田好花です！
`````
ポイントは以下の通りです。

- コンストラクタ関数を継承するにはextendsをしたクラスにコンストラクタ関数を書く。
- プロパティを追加する際はこのコンストラクタ関数の引数に追加します。上記だと`public from: string`が該当。
- 継承先でコンストラクタ関数を使う際は必ずsuper関数を使い、引数には継承元のプロパティを定義。
- メソッドを上書きしたい際は改めてメソッドを書く。

### protect修飾子
`private` は継承された先でも`private`であり続けます。
そういう際は`protected`を使います。
index_4-1.tsに変更を加えて確認します。

```typescript:継承元のコンストラクタ関数
constructor(public name: string, private catchphrase: string) {} // catchphraseをprivateに変更
```

```typescript:継承先のgreetingメソッド
  greeting() { // Property 'catchphrase' is private and only accessible within class 'Hinatazaka'.
    console.log(`${this.catchphrase} ${this.from}出身、日向坂46の${this.name}です！`);
  }
```
上記の通り、継承元のコンストラクタ関数の引数catchphraseをprivateに変更すると継承先の継承先のgreetingメソッドでエラーが起きます。先述したように`private` は継承された先でも`private`であり続けるためです。そこで`protected`を使います。`protected`は継承先を含むクラスの中でのみ読み書き可能になります。

```typescript:index_4-2.ts
class Hinatazaka {
  constructor(public name: string, protected catchphrase: string) {} // privateをprotectedに変更

  greeting(this: Hinatazaka) {
    console.log(`${this.catchphrase} 日向坂46の${this.name}です！`);
  }
}

class Member extends Hinatazaka {
  constructor(name: string, catchphrase: string, public from: string) {
    super(name, catchphrase);
  }

  greeting() { // エラーが出ない
    console.log(`${this.catchphrase} ${this.from}出身、日向坂46の${this.name}です！`);
  }
}

const matsuda = new Member('松田好花', '納豆大好き', '京都府');
matsuda.greeting(); // 納豆大好き 京都府出身、日向坂46の松田好花です！
```
変更点は`private`の部分を`protected`にしただけです。
これで継承先のエラーが解消されました。

## getterとsetter
`getter`は何かを取得したい、何かの関数を実行したい際に使います。
`setter`は何かの値を代入したい、何かの値を変更したい際に使います。
`getter`と`setter`を一緒に使う際は同じ型であることが必要です。
`getter`と`setter`を使うことによって各オブジェクトの値の管理がしやすくなります。

### getter
継承先で`getter`を定義し、_catchphraseを取得します。
`getter`には`get`キーワードを使い関数を定義します。
**`get`は返り値を指定しないとエラーが起こるので必ず返り値を定義します**。

```typescript:index_5-1.ts
class Hinatazaka {
  constructor(public name: string, public from: string) {}

  greeting(this: Hinatazaka) {
    console.log(`${this.from}出身、日向坂46の${this.name}です！`);
  }
}

class Member extends Hinatazaka {
  get catchphrase() { // getキーワードを使いcatchphrase関数を宣言
    if (!this._catchphrase) {
      throw new Error("There is no catchphrase");
    }
    return this._catchphrase // 値を返却
  }

  constructor(name: string, from: string, public _catchphrase: string) {
    super(name, from);
  }

  greeting() {
    console.log(`${this._catchphrase} ${this.from}出身、日向坂46の${this.name}です！`);
  }
}

const matsuda = new Member('松田好花', '京都府', '納豆大好き');
matsuda.greeting(); // 納豆大好き 京都府出身、日向坂46の松田好花です！
```

### setter
index_5-1.tsにsetterを追加します。
継承先で`setter`を定義し、_catchphraseの値を変更します。
`setter`には`set`キーワードを使い関数を定義します。
**`set` は必ず1つ以上の引数が指定しないと以下のエラーが起きるので必ず引数を定義します**。

```typescript:index_5-2.ts
class Hinatazaka {
  constructor(public name: string, public from: string) {}

  greeting(this: Hinatazaka) {
    console.log(`${this.from}出身、日向坂46の${this.name}です！`);
  }
}

class Member extends Hinatazaka {
  get catchphrase() {
    if (!this._catchphrase) {
      throw new Error("There is no catchphrase");

    }
    return this._catchphrase
  }

  set catchphrase(value) { // setキーワードを使いcatchphrase関数を宣言
    if (!value) {
      throw new Error('There is no subject')
    }
    this._catchphrase = value; // 値を代入
  }

  constructor(name: string, from: string, public _catchphrase: string) {
    super(name, from);
  }

  greeting() {
    console.log(`${this._catchphrase} ${this.from}出身、日向坂46の${this.name}です！`);
  }
}

const matsuda = new Member('松田好花', '京都府', '納豆大好き');
matsuda.catchphrase = 'やっほっす～'; // ここで実行、右辺の値がset catchphrase関数の引数になる
matsuda.greeting(); // やっほっす～ 京都府出身、日向坂46の松田好花です！
```

setterの代入が行われないとgetterの値（`new Memberの3番目の引数`）が返ってきます。
setterの代入が行われるとその値（`matsuda.catchphrase = 'やっほっす～'`）が返ってきます。

## static
`static`はクラスに静的メソッドや静的プロパティを定義します。
`static`を定義するとインスタンスを作らずににクラスを使うことができます。
`static`の宣言は static~の形で書きます。

```typescript
class Hinatazaka {
  static memberName = '松田好花'; // staticを宣言
}

Hinatazaka.memberName; // 松田好花
```

インスタンス化せずに`memberName`にアクセスできることが分かります。

## abstract
`abstract`は継承にのみ使えるクラスです。
`abstract`は`abstract`の中でしか使えません。
`abstract`クラスは他のクラスの継承元となるベースクラスでインスタンスを生成できません。
継承先でそれぞれ違う内容を定義したい際に有効です。

```Typescript:index_6.ts
abstract class Hinatazaka { // abstractを宣言
  constructor(public name: string, public from: string) {}

  greeting(this: Hinatazaka) {
    console.log(`${this.from}出身、日向坂46の${this.name}です！`);
    this.memberCatchphrase(); // abstractで定義したメソッドを実行
  }

  abstract memberCatchphrase(): void // メソッド abstractはabstractの中でしか使えない
}

class Member extends Hinatazaka {
  memberCatchphrase() { // 継承元のabstractの機能を定義
    console.log(`キャッチフレーズは${this._catchphrase}`);
  }

  get catchphrase() {
    if (!this._catchphrase) {
      throw new Error("There is no catchphrase");

    }
    return this._catchphrase
  }

  constructor(name: string, from: string, public _catchphrase: string) {
    super(name, from);
  }
}

const matsuda = new Member('松田好花', '京都府', '納豆大好き');
matsuda.greeting(); 
// 京都府出身、日向坂46の松田好花です！
// キャッチフレーズは納豆大好き
```

ポイントは以下の通りです。

- 継承元のクラスに`abstract`を宣言。
- 継承元のクラス内で更に`abstract`を宣言してメソッドを定義。
- 継承元のクラス内で`abstract`メソッド（`memberCatchphrase`）を実行（上記だとgreetingメソッド内）。
- 継承先で`memberCatchphrase`メソッドの機能を定義。

このように継承を前提に継承先で詳細な機能を定義できるので、複数のクラスを使う際の可読性を担保すると共に拡張性も向上します。

## シングルトンパターン
最後にこれまでの要素を応用して`シングルトンパターン`を実践します。
index_6.tsに変更を加えて確認します。
`シングルトンパターン`はクラスからインスタンスを1つしか作れなくするための方法です。

```typescript:index_7.ts
abstract class Hinatazaka {
  constructor(public name: string, public from: string) {}

  greeting(this: Hinatazaka) {
    console.log(`${this.from}出身、日向坂46の${this.name}です！`);
    this.memberCatchphrase();
  }

  abstract memberCatchphrase(): void
}

class Member extends Hinatazaka {
  private static instance: Member; // ポイント1
  memberCatchphrase() {
    console.log(`キャッチフレーズは${this._catchphrase}`);
  }

  get catchphrase() {
    if (!this._catchphrase) {
      throw new Error("There is no catchphrase");

    }
    return this._catchphrase
  }

  private constructor(name: string, from: string, public _catchphrase: string) { // ポイント2
    super(name, from);
  }

  static getInstance() { // ポイント3
    if (Member.instance) {
      return Member.instance;
    }
    Member.instance = new Member('松田好花', '京都府', '納豆大好き');
    return Member.instance;
  }
}

const matsuda = Member.getInstance(); // ポイント4
const konoka = Member.getInstance();
matsuda === konoka // true
```

ポイントごとに分解していきます。

```typescript:ポイント1
private static instance: Member;
```
instanceというフィールドを定義します。
staticにする理由はstaticメソッド（getInstance）からstaticプロパティを呼び出すためです。privateは外部からアクセスできないのでインスタンスの複製を防ぎます。
型はMemberの型を保持したいということになります。

```typescript:ポイント2
private constructor(name: string, from: string, public _catchphrase: string) {
  super(name, from);
}
```
コンストラクタ関数に`private`をつけると外部でnewを使ってインスタンスを作れなくなります。

```typescript:ポイント3
static getInstance() {
  if (Member.instance) { // 2回目以降の処理
    return Member.instance; 
  }
  Member.instance = new Member('松田好花', '京都府', '納豆大好き'); // 初回はこの部分が実行される
  return Member.instance; // 初回はこの値が返ってくる
}
```
インスタンスを使わずに外部から使用できるのはstaticメソッドになります。
よって最終的にはこの部分を呼び出し外部で実行します。
この中で行っているのは以下のとおりです。

1. 初回は`Member.instance`にインスタンス化したものを代入し、この値を返す。
2. 2回目以降は`private static instance: Member`に値があるのでif文の判定が**`true`**になりる。


```typescript:ポイント4
const matsuda = Member.getInstance();
```
ここで実行します。
結果、いくつインスタンスを作っても同じものが作られる`シングルトンパターン`が実現します。
参考までに変数`matsuda`の中身は下記の通りです。

```typescript
Member: {
  "name": "松田好花",
  "from": "京都府",
  "_catchphrase": "納豆大好き"
} 
```

`シングルトンパターン`のメリットは動的要因により何度もインスタンス化する際に、正しく動作させるかつメモリの節約になるといった点が挙げられます。

## あとがき
以上。TypeScriptにおけるclassについてつらと書いてみました。
TypeScriptに対する心構えは『常に初陣』（大泉氏リスペクト）。
次回は`interface`について書きたいと思います。