LoginSignup
0
0

[TypeScript] クラスについてまとめた

Last updated at Posted at 2024-04-28

はじめに

今回は クラスについてまとめました。
前回の記事もよろしければご確認ください。

クラスとは...?

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

クラスの宣言

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が有効な場合

初期化しない場合は以下のようなエラーが発生します。

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のチェックを無効にできます。

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

class Person {
   // OK
   name!: string

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

オプションプロパティを利用する

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

class Person {
   name?: string

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

読み取り専用プロパティ

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

 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 プロパティを持つオブジェクト型とみなします。

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インターフェースに準拠していることを表しています。

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の実装がない場合はエラーが発生します。

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クラス: ベースクラス
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はコンストラクタの最初に呼び出す必要があります。

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型とでオーバーライドしようとしているため、型エラーが発生します。

class Base {
    str?: string;
}

class Sub extends Base {
    str: number;

    constructor(str: string) {
        super();

        this.str = str;
    }
}

メソッドも同様です。
引数、戻り値の型がベースクラスと異なる場合、オーバーライドができません。

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()を定義することで、サブクラスに実装を強制させています。

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では、#を利用することでプライベートなメンバーを定義することができました。

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

public

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

private

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

protected

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

サンプルコード

privageValueはサブクラスで参照するとエラーが発生しますね。

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でもゲッターとセッターを定義できます。

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キーワードを指定することで静的なメンバーを作成することができます。

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

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

まとめ

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

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0