[TypeScript] クラスを使いこなそう！

### はじめに

今回は **クラス**についてまとめました。

### 【目次】
[まずは基本的な型宣言をマスターしよう！](https://qiita.com/PenPe/items/c189a611b3407b7ceef9)
[オブジェクト型の型宣言をマスターしよう！](https://qiita.com/PenPe/items/440683cc6e5e4ac0ca32)
[関数の型定義をマスターしよう！](https://qiita.com/PenPe/items/bff109c6f83f46d191a7)
[配列をマスターしよう！](https://qiita.com/PenPe/items/7e056eec2cce76a745a9)
[インターフェースをマスターしよう！](https://qiita.com/PenPe/items/f9314aeccb2f23cac382)
[クラスを使いこなそう！](https://qiita.com/PenPe/items/c8e6289f386c4db23a6f) ←🈁
[型修飾子についての理解を深めよう！](https://qiita.com/PenPe/items/7af57861e93830fe9a79)
[ジェネリックをマスターしよう！](https://qiita.com/PenPe/items/fde6a342e746e265d907)
[独自の構文拡張をマスターしよう！](https://qiita.com/PenPe/items/c79cdb5bc4a68a1dfd7f)

---

### クラスとは...？
クラスとは、オブジェクトの雛形（設計図）を定義したものです。
本記事では、初学者向けのクラスに関する細かな解説は省略させていただきます。

### クラスの宣言
TypeScriptでは以下の形式でクラスを宣言します。
```typescript
class Person {
    name: string;
    age: number;

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

    greet() {
        console.log("Hello");
    }
}
```

#### 上記サンプルコードについての解説
- クラスプロパティ
    - `name`, `age` 
- コンストラクタ（インスタンス作成時に実行）
    - `constructor() {...}`
- インスタンスメソッド
    - `greet()`

---

### 初期化チェック
TypeScriptでは、基本的にクラスプロパティはコンストラクタでの初期化が必要です。
※ `strictPropertyInitialization`が有効な場合

初期化しない場合は以下のようなエラーが発生します。
```typescript
class Person {
    // NG: Property 'name' has no initializer and is not definitely assigned in the constructor.
    name: string

    greet() {
        console.log(`Hello, My name is ${this.name}`);
    }
}
```

---

### 未割当のクラスプロパティ
コンストラクタでクラスプロパティを初期化しない場合は、以下いずれかの対応が必要となります。

#### 明確な割り当てアサーション をする
プロパティ名の後に `!` を追加することで`strictPropertyInitialization`のチェックを無効にできます。

`!`の意義ですが、プロパティを利用する前に値が必ず割り当てることを断言します。

 ```typescript
class Person {
    // OK
    name!: string

    greet() {
        console.log(`Hello, My name is ${this.name}`);
    }
}
 ```

 #### オプションプロパティを利用する
 明確な割り当てアサーションと別に、オプションプロパティを利用する方式もあります。
 そのクラスプロパティが省略可能である場合は、明確な割り当てアサーション（`!`）ではなく、オプションプロパティ（`?`）を利用します。
 
 ```typescript
 class Person {
    name?: string

    greet() {
        console.log(`Hello, My name is ${this.name}`);
    }
}
 ```

---

### 読み取り専用プロパティ
インターフェースと同様、クラスプロパティも`readonly`を使用して、読み取り専用の値を定義できます。
読み取り専用のプロパティは宣言時および、コンストラクタでのみ初期化が可能です。

```typescript
 class Person {
    readonly name: string;

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

let taro = new Person("taro");

// NG: Cannot assign to 'name' because it is a read-only property.
taro.name = "saburo"
```

---

### クラス型について
TypeScriptにおけるクラスの型チェックですが、
**クラスを全てのメンバーを含む任意のオブジェクト型**として扱います。

サンプルコードを紹介します。
以下のコードにて、TypeScriptは`Person`クラスを`name` プロパティを持つオブジェクト型とみなします。
```typescript
class Person {
    readonly name: string;

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

let taro: Person 
taro = new Person("taro"); // OK

let saburo: Person;
saburo = { name: "saburo" } // OK
```

そのため、`saburo = { name: "saburo" }` は `Person`型と一致するとみなされ型エラーが発生しないということです。

この仕様はTypeScriptの型システムを扱う上での重要な概念として覚えておきましょう。

---

### クラスとインターフェース
クラスがインターフェースに準拠していることを宣言するには、
`implements`キーワードを追加し、その後にインターフェースを記載します。

以下のサンプルコードでは、`Taro`クラスが`Student`インターフェースに準拠していることを表しています。
```typescript
interface Student {
    study(hour: number): void
}

class Taro implements Student {
    study(hour: number) {
        console.log(`I'll study for ${hour} hours.`)
    }
}
```

`Student`インターフェースに準拠することで`Taro`クラスは`study(hour: number): void`の実装が強制されます。

`study(hour: number): void`の実装がない場合はエラーが発生します。

```typescript
interface Student {
    study(hour: number): void
}

// NG: Class 'Taro' incorrectly implements interface 'Student'.
class Taro implements Student {
    hello() {
        console.log("my name is Taro")
    }
}
```

---

### クラスの拡張
TypeScriptでは`extends`キーワードを用いることでクラスの拡張が可能です。

以下のサンプルコードでは、`Taro`クラスが`Person`クラスを継承しています。
- `Taro`クラス: サブクラス
- `Person`クラス: ベースクラス

```typescript
class Person {
    name: string;
    age: number;

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

class Taro extends Person {
    hello() {
        console.log("my name is Taro")
    }
}

let taro = new Taro("taro", 15);
taro.hello(); // OK: "my name is Taro" 
```

---

### サブクラスのコンストラクタ
サブクラスは独自のコンストラクタを定義する必要はありません。
コンストラクタを持たないサブクラスは、暗黙的にベースクラスのコンストラクタを実行します。

**コンストラクタをオーバーライドした場合**、`super`キーワードを使ってベースクラスのコンストラクタを明示的に呼び出す必要があります。

`super`はコンストラクタの最初に呼び出す必要があります。
```typescript
class Person {
    name: string;
    age: number;

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

class Taro extends Person {
    hobby: string;

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

    hello() {
        console.log(`my name is ${this.name}. my hobby is ${this.hobby}`)
    }
}

let taro = new Taro("taro", 15, "baseball");
taro.hello(); // "my name is taro. my hobby is baseball" 
```

---

### サブクラスのメソッド, プロパティ
サブクラスでは、ベースクラスにないメソッド, プロパティを新たに宣言できます。
すでにベースクラスにあるメソッド, プロパティを再宣言（**オーバーライド**）することも可能です。

#### オーバーライドの注意点
メソッド, プロパティをオーバーライドする場合、ベースクラスの型に割当可能である必要があります。

以下のサンプルコードでは、`Base`クラスで`string`型として定義された`str`プロパティを、`Sub`クラスで、`number`型とでオーバーライドしようとしているため、型エラーが発生します。

```typescript
class Base {
    str?: string;
}

class Sub extends Base {
    str: number;

    constructor(str: string) {
        super();

        this.str = str;
    }
}
```

メソッドも同様です。
引数、戻り値の型がベースクラスと異なる場合、オーバーライドができません。
```typescript
class Base {
    sampleFn(input :string[]) {
        //...
    }
}

class Sub extends Base {
    // NG: Property 'sampleFn' in type 'Sub' is not assignable to the same property in base type 'Base'.
    sampleFn(input: number[]) {
        //...
    }
}
```

---

### 抽象クラス
JavaScriptではサポートされていませんが、
TypeScriptでは、抽象クラスを作成することができます。

#### 抽象クラスとは？
抽象クラスとは **インスタンス化することができず、スーパークラスとして利用することを保証されるクラス**です。
サブクラスに抽象定義したメソッドの実装を強制させます。

#### サンプルコード
以下の例では、抽象クラス`School`を作成し、クラス内で抽象メソッド`getSchoolType()`を定義することで、サブクラスに実装を強制させています。

```typescript
abstract class School {
    readonly name: string;

    constructor(name: string) {
        this.name = name
    }

    abstract getSchoolType(): string;
}

class JuniorSchool extends School {
    getSchoolType() {
        return "junior high school";
    }
}

class HighSchool extends School {
    getSchoolType() {
        return "high school";
    }
}

// NG: Non-abstract class 'ElementarySchool' does not implement all abstract members of 'School'
class ElementarySchool extends School {
}
```

---

### アクセス修飾子
JavaScriptでは、`#`を利用することでプライベートなメンバーを定義することができました。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Classes/Private_properties

TypeScriptでは上記に加え、`public`, `private`, `protected`といったアクセス修飾子がサポートされています。

#### public
どこからでもアクセス可能
アクセス修飾子を指定しない場合、メンバーはpublicとなります。

#### private
クラス内部からのみアクセス可能

#### protected
クラス内部 + サブクラスからのみアクセス可能

#### サンプルコード
`privageValue`はサブクラスで参照するとエラーが発生しますね。

```typescript
class Klass {
    public publicValue: string;
    private privageValue: string;
    protected protectedValue: string;

    constructor(publicValue: string, privateValue: string, protectedValue: string) {
        this.publicValue = publicValue;
        this.privageValue = privateValue;
        this.protectedValue = protectedValue;
    }

    setPrivateValue(privageValue: string) {
        this.privageValue = privageValue;
    }
    
    setProtectedValue(protectedValue: string) {
        this.protectedValue = protectedValue;
    }
}

class SubKlass extends Klass {
    //NG: Property 'privageValue' is private and only accessible within class 'Klass'.
    setPrivateValueBySubKlass(privageValue: string) {
        this.privageValue = privageValue;
    }

    setProtectedValueBySubKlass(protectedValue: string) {
        this.protectedValue = protectedValue;
    }
}
```

---

### ゲッター / セッター
JavaScriptと同様、TypeScriptでもゲッターとセッターを定義できます。

```typescript
class User {
    email: string;
    private _password: string;

    constructor(email: string, _password: string) {
        this.email = email;
        this._password = _password
    }

    get password(): string {
        return this._password;
    }

    set password(password: string) {
        if(password.length < 8) {
            return;
        }

        this._password = password;
    }
}

let taro = new User("taro@example.co.jp", "Taro1234");

// OK
taro.email;
taro.password;

// NG: Property '_password' is private and only accessible within class 'User'.
taro._password;

// Not Updated
taro.password = "Hoge"
console.log(taro.password) // => "Taro1234"

// Updated
taro.password = "Taro9876"
console.log(taro.password) // => "Taro9876"
```

---

### static
JavaScriptでは`static`キーワードを指定することで静的なメンバーを作成することができます。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Classes/static

TypeScriptでは`static`キーワードと、`readonly`, `public`, `private`, `protected`修飾子を合わせて使うことが可能です。

```typescript
class Klass {
    private static readonly className:string = "Klass"
}
```

---

### まとめ
以上です！
基本的な機能については本記事で網羅できていると思います。
覚えることが多いですが、がんばりましょう！

次の記事もよければご覧ください🙏

https://qiita.com/PenPe/items/7af57861e93830fe9a79