TypeScript クラスを使ってみる

作成日:2023年11月12日

関連ページ

参考サイト

JavaScript 参考サイト

クラスの定義

クラスを使うには class 構文(クラス宣言文またはクラス式)を使ってクラスを定義します。

クラス宣言文は class キーワードを使って、class クラス名{ } のようにクラスの構造を定義します。

クラス名には任意の(識別子として有効な)文字列を使用できますが、慣習としてクラス名には大文字ではじまる名前をつけます。

// クラス宣言文
class MyClass {
  // クラスの定義の本体
}

または、以下のようにクラス式を使って定義することもできます。

※ 但し、クラス式を使ってクラスを定義する場合、TypeScirpt ではそのクラスのが型が作成されないなどの違いがあるので、通常 TypeScirpt でクラスを定義する場合はクラス宣言文を使います。

// クラス式
const MyClass = class MyClass {
  // ...
};

// クラス式ではクラス名を省略可能
const MyClass = class {
  // ...
};

new 演算子でインスタンス化

クラスは new 演算子を使ってそのクラスのオブジェクト(インスタンス)を作成することができます。クラスからインスタンスを作成することをインスタンス化と呼びます。

class MyClass {
  // ...
}

const myClass = new MyClass(); // new 演算子で MyClass のオブジェクトを作成

クラスの型注釈

TypeScript では、クラスを定義するとクラス名と同じ名前の型が同時に定義(作成)されます。

インスタンスを代入する変数に型注釈を書く場合はクラス名を使います(クラス名がそのクラスの型名でもあります)。

const myClass: MyClass = new MyClass();

関連項目:クラスの型

コンストラクタ

コンストラクタはクラスからインスタンスを作成する際に呼び出されるメソッド(関数)で、プロパティなどインスタンスの状態を初期化するのに使用されます(初期化以外の処理も可能です)。

コンストラクタはクラス宣言の中で constructor という名前のメソッドとして定義します。

以下の場合、コンストラクタの処理はコンソールへ文字列を出力するだけのもので意味はありせんが、new 演算子でオブジェクトを生成する際に呼び出されてコンソールに文字列が出力されます。

class MyClass {
  // コンストラクタの定義
  constructor() {
    console.log('MyClass instance is created.');
  }
}

const myClass = new MyClass();  // MyClass instance is created と出力される

コンストラクタは引数を受け取ることができます。

TypeScript でのコンストラクタの引数の型注釈は、通常の関数の引数の型注釈と同じです。

また、コンストラクタが引数を受け取る場合は、new でインスタンスを生成する際にそれに応じた引数を渡す必要があります。

class MyClass {
  // 引数には型注釈を書きます
  constructor(name: string) {
    console.log('Hello ' + name);
  }
};

//コンストラクタの引数に対応した引数を渡す必要があります。
const myClass = new MyClass('foo');  // Hello foo と出力される
const myClass2 = new MyClass(2);  // 引数の型が異なるのでコンパイルエラー

コンストラクタ関数の戻り値の型注釈は書けません(通常、コンストラクタに戻り値はありません)。

コンストラクタの省略

コンストラクタの処理が不要な場合はコンストラクタの記述を省略できます。

省略した場合でも自動的に空のコンストラクタが定義され、クラスにはコンストラクタが必ず存在します。

プロパティの宣言

クラス宣言(class 構文)の中でクラスのインスタンスのプロパティを宣言することができます。

プロパティは以下の構文(ES2022 で導入されたクラスフィールド構文)で定義することができます。

TypeScriptでは、プロパティの型注釈を書きます。

class クラス {
  プロパティ名: 型 = プロパティの初期値;
}

クラス宣言の中でプロパティを宣言すると、そのクラスのインスタンスは自動的にそのプロパティを持った状態で作成されます。

以下の User クラスは string 型のプロパティ name と number 型のプロパティ age を持ち、初期値はそれぞれ '' と 0 です。

class User {
  // プロパティ名: 型 = 初期値(式);
  name: string = '';
  age: number = 0;
}

const user = new User();
console.log(user.name);  // ''
console.log(user.age);  // 0

user.name = 'foo';
user.age = 33;

console.log(user.name);  // 'foo'
console.log(user.age);  // 33

初期化子 (initializer)

プロパティの初期値を設定する部分(プロパティ名の右の = 値)を初期化子と呼びます。

初期化子により値の型が自明な場合、コンパイラーはプロパティの型を推論してくれるので、初期化子を伴うプロパティ宣言では型注釈を省略することができます。

言い換えるとプロパティ宣言で値を設定している場合、型注釈を省略することができます。

先述の例は、以下のように型注釈を省略することもできます。

class User {
  // 初期化子でプロパティの初期値を設定
  name= '';  // string 型と推論される→型注釈を省略可能
  age = 0;  // number 型と推論される→型注釈を省略可能
}

プロパティとフィールド

ここではプロパティという用語を使用していますが、TypeScript と ES2022 ではクラスに対して宣言されるプロパティ(クラスブロック直下に変数として定義するプロパティ)は正確にはフィールドと呼びます。

JavaScript Primer: [ES2022] Publicクラスフィールド

プロパティの初期値を省略

クラスフィールド構文では、プロパティの初期値(初期化子)は省略可能となっています。

但し、TypeScirpt ではコンパイラーオプションの strict が true の場合、省略すると strictNullChecks と strictPropertyInitialization が有効になっているためコンパイルエラーになります。

class User {
  name: string ; // コンパイルエラー
  //Property 'name' has no initializer and is not definitely assigned in the constructor.(name プロパティには初期化子がなく、コンストラクタでも指定されていません)
}

プロパティの初期値を省略した場合、そのプロパティは undefined で初期化されます(インスタンス生成後そのプロパティは undefined になります)。

コンパイルエラーを回避するには以下のような方法があります。

  • プロパティの型注釈を undefined とのユニオン型にする
  • プロパティをオプショナルにする
  • コンストラクタでプロパティを初期化する
class User {
  // プロパティの型注釈を undefined とのユニオン型にする
  name: string | undefined ;
}

オブジェクト型と同様、プロパティ名の後ろに ? を付けることでオプショナルなプロパティを宣言することができます。オプショナルなプロパティの型は指定したと undefined 型とのユニオン型になります。

class User {
  // プロパティをオプショナルにする
  name?: string ;  // 型推論により string | undefined 型になる
}

コンストラクタの中でプロパティを初期化(初期値を代入)すればコンパイルエラーになりません。

コンストラクタ関数内での this は新しく作成されるオブジェクトのインスタンスです。インスタンスのプロパティを参照するには、this.プロパティ名 とします。

class User {
  name: string ;
  // コンストラクタでプロパティを初期化
  constructor() {
    this.name = "";
  }
}

コンストラクタに引数を持たせれば、インスタンス生成時にプロパティの値を動的に指定できます。

以下の場合、プロパティ age はオプショナルにしているので、コンストラクタの引数もオプショナルな引数にしています。

class User {
  name: string ;
  // オプショナルなプロパティ
  age?: number;

  // プロパティを初期化(age はオプショナル引数)
  constructor(name: string, age?: number) {
    this.name = name;
    this.age = age;
  }
}

const foo = new User('foo');
console.log(foo.name);  // 'foo'
console.log(foo.age);  // undefined(プロパティの値がない場合は undefined)
const bar = new User('bar', 28);
console.log(bar.name);  // 'bar'
console.log(bar.age);  // 28

コンストラクタ以外でプロパティを初期化

TypeScript コンパイラは、プロパティ定義またはコンストラクタでプロパティが初期化されるかをチェックしますが、コンストラクタ以外のメソッドで初期化されるところまでは追いかけません。

そのため、以下はコンパイルエラーになります。

class User {
  name: string ; // コンパイルエラー
  // Property 'name' has no initializer and is not definitely assigned in the constructor.

  constructor() {
    // initialize() を呼び出す
    this.initialize();
  }
  // コンストラクタ以外でプロパティを初期化
  initialize() {
    this.name = ''; // name プロパティを初期化
  }
}

このような場合は、プロパティ名の末尾に !明確な割り当てアサーション)を指定することで、コンストラクタ以外の場所で初期化していることを TypeScript に伝えることができます。

class User {
  name!: string ; // OK
  constructor() {
    this.initialize();
  }
  initialize() {
    this.name = '';
  }
}
読み取り専用プロパティ

オブジェクト型と同様、プロパティ名の前に readonly キーワードを指定することで、そのプロパティを読み取り専用にすることができます。

読み取り専用に指定したプロパティは、再代入しようとするとコンパイルエラーになります。

class User {
  readonly name: string = '' ;
}

const user = new User();
user.name = 'foo'; // コンパイルエラー
// Cannot assign to 'name' because it is a read-only property.

コンストラクタで読み取り専用プロパティに代入

コンストラクタ内では自身の読み取り専用プロパティに代入することができます。※コンストラクタ以外のメソッドでは代入はできません。

class User {
  // 読み取り専用プロパティ
  readonly name: string = '' ;
  // コンストラクタでは読み取り専用プロパティに代入可能
  constructor(name: string) {
    this.name = name;
  }
}

const user = new User('foo');
console.log(user.name);
user.name = 'bar'; // 再代入はコンパイルエラー

メソッドの宣言

クラス宣言(class 構文)の中でメソッドの宣言も書くことができます。

プロパティ同様、クラス宣言の中でメソッドを宣言すると、そのクラスのインスタンスは自動的にそのメソッドを持った状態で作成されます。

メソッドは以下のような構文で定義できます。

メソッドの型注釈は関数宣言構文の型注釈と同じで、引数がある場合は引数の型注釈は必要ですが、戻り値の型注釈は省略できます。

class クラス {
  メソッド(引数: 型): 戻り値の型 {
    // 処理
  }
}

メソッドの宣言の構文は、オブジェクトリテラルのメソッドの短縮記法と同じですが、オブジェクトリテラルとは異なり、複数のメソッドを宣言する場合にカンマで区切る必要はありません。

また、メソッドの中からクラスのインスタンスを参照するには、コンストラクタと同様 this を使います。

以下では Counter クラスに increment と add、isCountOver10 の3つのメソッドを定義しています。

new で作成した Counter クラスのインスタンスは increment メソッドと add メソッド、isCountOver10 メソッドを持つようになります。isCountOver10 には戻り値の型注釈を書いていますが省略可能です。

class Counter {
  count: number = 0;

  increment() {
    this.count++;
  }

  add(num: number) {
    this.count += num;
  }

  isCountOver10(): boolean {
    return this.count > 10;
  }
}

const counter = new Counter();
console.log(counter.count); // 0
counter.increment();
console.log(counter.count); // 1
console.log(counter.isCountOver10()); // false
counter.add(10);
console.log(counter.count); // 11
console.log(counter.isCountOver10()); // true

アクセス修飾子

TypeScirpt では、クラス宣言内のプロパティ宣言及びメソッド宣言(コンストラクタを含む)に以下のアクセス修飾子を指定することができます。

アクセス修飾子 説明
public どこからもアクセス可能
private クラスの内部からのみアクセス可能
protected クラスの内部とサブクラス(子クラス)からアクセス可能
なし public と同じ

デフォルトではすべてのメンバ(プロパティとメソッド)を public とみなすので、明示的に public キーワードを使用する必要はありませんが、指定することもできます。

以下の例では age プロパティを private としています。

最後の行の foo.age はクラスの定義の外でのアクセスなのでコンパイルエラーとなります。

constructor や isOver18 メソッドで age プロパティにアクセスしていますが、これは User クラスの定義内なので許可されます。

class User {
  name: string;
  // クラスの内部からのみアクセス可能
  private age: number;

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

  isOver18(): boolean {
    return this.age > 18;
  }
}

const foo = new User('foo', 33);
console.log(foo.isOver18()); // true
console.log(foo.age); // コンパイルエラー(private を付けているので外部からアクセス不可)
// Property 'age' is private and only accessible within class 'User'.

コンストラクタの短縮記法

コンストラクタの引数名にアクセス修飾子を付けることで、プロパティ宣言を省略することができます。

メソッドの引数にはアクセス修飾子を設定することはできませんが、コンストラクタの引数にはアクセス修飾子を付けることがでます。

コンストラクタの引数名の前にアクセス修飾子を付けることで、コンストラクタの引数であると同時にプロパティ宣言とみなされます。

以下は name プロパティと age プロパティを持つ User クラスですが、プロパティの初期化はコンストラクタで行っています。

class User {
  name: string;
  private age: number;

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

上記はコンストラクタの短縮記法を使うと以下のように記述できます。

コンストラクタの引数名の前にアクセス修飾子を付けることでプロパティ宣言とみなされ、コンストラクタの引数の外に public name: string;private age: number; を記述するのと同じ効果があります。

また、コンストラクタに渡された引数が自動的に初期値として設定されるので、this.name = name;this.age = age; が自動的に行われます。

class User {
  constructor(public name: string, private age: number) {
    console.log(this.name, this.age);
  }
}

プライベートプロパティ

TypeScirpt の private アクセス修飾子とは別に、JavaScript の ES2022 で導入されたプロパティ名の前に # を付けるプライベートなプロパティを定義する構文も利用することができます。

# を付けたプロパティは private アクセス修飾子と同様、そのクラスの内部でのみアクセス可能です。

定義したプライベートプロパティは、this.#プロパティ名のように#を含んだプロパティ名で参照します。

以下は age プロパティを # を付けてプライベートプロパティに設定した例です。

private アクセス修飾子と同様、クラス内部からはアクセス可能ですが、外部からはアクセスするとエラー(JavaScript の SyntaxError と TypeScirpt のコンパイルエラー)になります。

class User {
  name: string;
  #age: number;

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

  isOver18(): boolean {
    return this.#age > 18;
  }
}

const foo = new User('foo', 33);
console.log(foo.isOver18()); // true
console.log(foo.#age);  // SyntaxError とコンパイルエラー

静的プロパティと静的メソッド

クラス宣言には、インスタンスではなくクラス自体に定義する静的プロパティやクラスをインスタンス化せずに利用できる静的メソッド(クラスメソッド)も定義することができます。

静的プロパティと静的メソッドはインスタンスではなく、クラス自身に属するプロパティとメソッドです。いずれも通常の宣言に static を付けて宣言するだけです。

静的プロパティ

静的プロパティは、プロパティの前に static を付けて宣言します。定義した静的プロパティは、クラス自体のプロパティとして定義されます。

以下は、静的プロパティを使って MyClass クラス自体にプロパティを定義しています。

class MyClass {
  // static を付けると静的プロパティになる
  static myNumber: number = 123;
}

// クラスのプロパティとして参照
console.log(MyClass.myNumber);  // 123
MyClass.myNumber = 456;
console.log(MyClass.myNumber);  // 456

静的プロパティの型推論

静的プロパティは初期値がセットされている場合、その初期値からプロパティの型が型推論されるので、プロパティの型注釈を省略することもできます。

静的メソッド

静的メソッドは、メソッドの前に static を付けます。

定義した静的メソッドは、クラス自体のメソッドとして定義されます。静的メソッドの中での this はクラスオブジェクト(以下の場合は MyClass)になります。

class MyClass {
  static myNumber: number = 123;
  // 静的メソッド
  static printMyNummber() {
    console.log(this.myNumber); // console.log(MyClass.myNumber); でも同じ
  }
}

MyClass.printMyNummber(); // 123

※ コンストラクタの中では this はインスタンスになるので、クラス名.静的プロパティ名 で参照できます。

class MyClass {
  static myNumber: number = 123;

  constructor() {
    // コンストラクタの中では クラス名.静的プロパティ名 で参照
    console.log(`myNumber is ${MyClass.myNumber}`);
  }
}

const myClass = new MyClass(); // myNumber is 123

アクセス修飾子

TypeScript の静的プロパティはアクセス修飾子やプライベートプロパティと組み合わせて使用できます。

class MyClass {
  // 静的プロパティとプライベートプロパティ
  static #privateNumber: number = 123456;
  // 静的メソッド
  static printPrivateNummber() {
    // クラス内からは参照できる
    console.log(this.#privateNumber);
  }

  // アクセス修飾子と静的プロパティ
  private static privateString: string = 'secret';
  // 静的メソッド
  static printPrivateString() {
    // クラス内からは参照できる
    console.log(this.privateString);
  }
}

MyClass.printPrivateNummber(); // 123456
MyClass.printPrivateString(); // secret
// クラス外からは参照できない
console.log(#privateNumber); // コンパイルエラー
console.log(privateString); // コンパイルエラーと ReferenceError エラー

また、TypeScript の静的メソッドはアクセス修飾子を組み合わせて使用することができます。

class MyClass {
  static myNumber: number = 123;

  // private アクセス修飾子を付けた静的メソッド
  private static getMyNummber() {
    return this.myNumber;
  }

  // 通常の静的メソッド
  static printMyNummber() {
    // クラス内からは private を付けた静的メソッドを参照できる
    console.log(this.getMyNummber());
  }
}

MyClass.printMyNummber(); // 123
// クラス外からは参照できない
const myNumber = MyClass.getMyNummber(); // コンパイルエラー
// Property 'getMyNummber' is private and only accessible within class 'MyClass'.

読み取り専用の静的プロパティ

静的プロパティに readonly 修飾子を付けることで読み取り専用になります。

class MyClass {
  // 読み取り専用の静的プロパティ
  static readonly myNumber: number = 123;
}

console.log(MyClass.myNumber);  // 123
// 読み取り専用なので再代入できない
MyClass.myNumber = 456; // コンパイルエラー

静的初期化ブロック

静的初期化ブロックは ES2022 で導入された機能で、クラス宣言の中の static { } というブロックの中で静的プロパティの初期化処理などの文を記述することができます

以下は意味のない例ですが、宣言した静的プロパティに静的ブロックの中で乱数を使って初期値を設定し、値が5より大きければコンソールに「You are lucky today!」と出力します。

class MyClass {

  static luckyNumber: number;

  static {
    this.luckyNumber = Math.floor(Math.random() * 10);
    if(this.luckyNumber > 5) {
      console.log('You are lucky today!')
    }
  }
}
console.log(MyClass.luckyNumber); // 乱数で生成された値が出力される

型引数を持つクラス(ジェネリッククラス)

ジェネリック型やジェネリクスのように、クラスも型引数を持つことができます。

型引数を持つクラスをジェネリッククラス(Generic Classes)と呼びます。

ジェネリッククラスの宣言では、クラス名の後に <T> のように型パラメータを追加します。

型パラメータは型引数を< >で囲みます。型引数の名前は一般的に T がよく使われますが、任意の文字列を名前として使用できます。また、複数ある場合はカンマ区切りで指定します。

クラスの型引数はクラスの定義内で使用することができます。

class クラス名<型引数> {
  // 定義の中で型引数を参照できます
}

インスタンスごとに異なる型を保持できるようにしたい場合に、ジェネリッククラスを使うと便利です。

以下は簡単なジェネリッククラスの例です。以下の MyClass は T という型引数を持っています。

class MyClass<T> {
  value: T;

  constructor(value: T) {
    this.value = value;
  }

  getValue(): T {
    return this.value;
  }
}

// 使用例
const num = new MyClass<number>(34); // number
const str = new MyClass<string>("Hello, TypeScript!");; // string
const bool = new MyClass(true); // boolean(型パラメータを省略)

console.log(num.getValue()); // 34
console.log(str.getValue()); // Hello, TypeScript!
console.log(bool.getValue()); // true

クラスの定義の中では、プロパティやコンストラクタの引数、メソッドの戻り値の型注釈で必要に応じて型引数 T を参照(型変数として使用)することができます。

そしてコンストラクタの呼び出し時にnew クラス名<型>(引数)のように< >に型引数を指定して、インスタンスを作成することができます。

この例の場合、コンストラクタの引数の型が T となっているので、型引数 T に number を指定したら、コンストラクタの引数には number 型の値を渡す必要があります。

但し、ジェネリクス(型引数を持つ関数)の呼び出し側で型パラメータが省略可能なのと同様、new でクラスのインスタンスを生成する場合も型パラメータを省略することができます(16行目)。

以下は簡単なキューのクラス(Queue)をジェネリックを使用して実装した例です。このキューは異なる型の要素を格納できるようになっています。

class Queue<T> {
  private items: T[] = [];

  enqueue(item: T): void {
    // 配列(キュー) items の最後に受け取った要素を追加
    this.items.push(item);
  }

  dequeue(): T | undefined {
    // 配列 items の先頭の要素を取り除いて返す
    return this.items.shift();
  }

  getSize(): number {
    // 配列 items の長さを返す
    return this.items.length;
  }
}

// 使用例
const numberQueue = new Queue<number>(); // number 型のキュー
numberQueue.enqueue(1);
numberQueue.enqueue(2);
numberQueue.enqueue(3);

console.log(numberQueue.getSize()); // 3
console.log(numberQueue.dequeue()); // 1
console.log(numberQueue.getSize()); // 2

const stringQueue = new Queue<string>(); // string 型のキュー
stringQueue.enqueue("apple");
stringQueue.enqueue("banana");
stringQueue.enqueue("orange");

console.log(stringQueue.getSize()); // 3
console.log(stringQueue.dequeue()); // apple
console.log(stringQueue.getSize()); // 2

以下はジェネリックを使用した汎用的なデータのリストを扱う List クラスの例です。

class List<T> {
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  get(index: number): T | undefined{
    return this.items[index];
  }

  getAll(): T[] {
    return this.items;
  }
}

// 使用例
const numberList = new List<number>();
numberList.add(1);
numberList.add(2);
numberList.add(3);

console.log(numberList.get(1));  // 2
console.log(numberList.get(3));  // undefined
console.log(numberList.getAll()); // [1, 2, 3]


const stringList = new List<string>();
stringList.add("apple");
stringList.add("banana");
stringList.add("orange");

console.log(stringList.get(0));  // "apple"
console.log(stringList.getAll()); // ["apple", "banana", "orange"]

静的メンバーには型引数は使えない

ジェネリッククラスは、インスタンス側でのみジェネリックであるため、静的メンバー(静的プロパティや静的メソッド)はクラスの型パラメーター(型引数)を使用できません。

関連項目

クラスの型

TypeScript ではクラス宣言でクラスを定義するとそのクラス名と同じ名前の型も同時に作成されます。

例えば、以下のようにクラス宣言で User というクラスを作成すると、同時に同じ名前の User 型も作成されます。VS Code で変数 foo にマウスオーバーするとインスタンスの型 User が表示されます。

class User {
  name: string;
  age: number;

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

  greet(): string {
    return `Hello, my name is ${this.name}.`;
  }
}

const foo = new User('Foo', 22); // foo は User 型のインスタンス

クラス宣言で作成された型(この場合は User)は普通の型と同じように、型注釈に使えるので以下のようにインスタンス(変数)の型注釈として記述することができます。

const foo: User = new User('Foo', 22);

※ 但し、クラス式でクラスを作成する場合は、User 型は作成されません。

クラスの構造

上記のクラス User 型の構造は「string 型のプロパティ name と number 型のプロパティ age を持ち、引数無しで string 型の値を返す () => string 型のメソッド greet を持つ」ということになります。

TypeScirpt は「構造が同じなら同じ型と見なす」という構造的型付け(Structural Typing)を採用しているので、new User で作成しないオブジェクト(User クラスのインスタンスではないオブジェクト)でも構造が同じであれば、User 型として扱うことができます。

そのため、以下の変数 bar のように new User で作成しないオブジェクトでも「string 型の name と number 型の age を持ち、() => string 型のメソッド greet を持つ」構造のオブジェクトは User 型として扱えます。

class User {
  name: string;
  age: number;

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

  greet(): string {
    return `Hello, my name is ${this.name}.`;
  }
}

// User 型のオブジェクト
const bar: User = {
  name: 'bar',
  age: 25,
  greet(): string {
    return `Hello, I am ${this.name}.`;
  }
}

TypeScirpt は構造的型付けを採用しているので、構造が同じであれば同じ型だと見なすため、次のようなコードが成立します。

異なる名前の型でも構造が同じだと代入できてしいます。但し、Pet 型の変数 pet には Person 型のインスタンスが代入されていますが Person 型となっているので注意が必要です(22と23行目)。

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

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

// Person 型のインスタンスの作成
const person: Person = new Person('Foo');

// Pet 型の変数 pet に Person 型のインスタンスが代入可能
const pet: Pet = person;

console.log(pet.name);  // Foo
console.log(pet);  // Person {name: 'Foo'}
console.log(pet instanceof Person); // true
console.log(pet instanceof Pet); // false

また、完全に同じ構造でなくても、代入先の型を満たす部分型関係にあれば以下のような代入も可能です。以下の場合、Pet 型には age プロパティがないので 行目はコンパイルエラーになります。

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

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

const person: Person  = new Person('Foo', 32);

const pet: Pet = person;

console.log(pet instanceof Person); // true
console.log(pet instanceof Pet); // false
console.log(person.age);  // 32
console.log(pet.age);  // コンパイルエラー
// Property 'age' does not exist on type 'Pet'.

public でないプロパティがある場合

但し、クラスに1つでも public でないプロパティがあると、プロパティが全く同じでも、そのクラスは nominal Typing(公称型クラス)として扱われてしまうので、以下の代入はコンパイルエラーになります。

class Person {
  name: string ;
  private age: number;  // private
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

class Pet  {
  name: string = ''
  private age: number;  // private
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

const person: Person  = new Person('Foo', 32);

const pet: Pet = person;  // コンパイルエラー
// Type 'Person' is not assignable to type 'Pet'.
// Property 'age' is private in type 'Person' but not in type 'Pet'.

型引数を持つ場合

型引数を持つ場合は、クラス宣言によって作られる型も型引数を持つ型になります。

class Person<T> {
  name: string;
  data: T;
  constructor(name: string, data: T) {
    this.name = name;
    this.data = data;
  }
}
// 型引数を持つ型で型注釈
const foo: Person<string> = new Person('foo', 'some data');
// const foo = new Person('foo', 'some data'); // 型注釈は省略可能

// Person<string> 型のオブジェクト
const bar: Person<string> = {
  name: 'bar',
  data: 'bar data',
}

// Person<number> 型のオブジェクト
const baz: Person<number> = {
  name: 'baz',
  data: 123,
}

変数名と型名の名前空間

TypeScirpt では変数名と型名という2種類の名前があり、それぞれが別々の名前空間に属しています(別々に管理されています)。

プログラムの中では識別子を使って参照しますが、識別子はそれが書かれたコンテクストによって変数名であるか型名であるかが判断されます。

クラス宣言は両方の名前空間に同時に同じ名前を作成しますが、例えば、以下の場合、11行目の左辺の User は型名、右辺の User は変数名(class User の User)になります。

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

// 左辺の User は型名、右辺の User は変数名
const foo: User = new User('Foo', 22);

// 以下の User は変数名(JavaScript のクラスは関数オブジェクト)
console.log(typeof User);  // function

コンストラクタの型

クラスのインスタンスは、new を使った式によりコンストラクタ関数が呼び出されて作成されますが、TypeScirpt ではコンストラクタ関数の型を表現することができます。

以下が特定のシグネチャを持つコンストラクタ関数を表す構文です。関数の型宣言に似ていますが、引数の前に new が付き、インスタンスの型を返します(construct signature)。

type コンストラクタの型 = new (引数リスト) => インスタンスの型;

例えば、以下の PersonCtor のように Person クラスのコンストラクタ関数の型を表すことができます。

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

type PersonCtor = new (name:string, age:number) => Person;

この場合、PersonCtor は、文字列と数値を引数に取り、Personのインスタンスを返す新しいコンストラクタ関数を表します。

PersonCtor 型の変数には、そのシグネチャに一致する任意のコンストラクタ関数を割り当てることができます。この場合、Person クラスのコンストラクタはそのシグネチャに一致するので、その条件を満たしています。したがって、以下のようなコードが可能になります。

// PersonCtor 型の変数に Person クラス(のコンストラクタ)を割り当てられる(代入できる)
const ctor: PersonCtor = Person;
const person = new ctor('John Doe', 30);

上記の例では、Person クラスのコンストラクタは PersonCtor 型の変数 ctor に割り当てられています(PersonCtor 型の変数に Person クラスを代入しています)。

そして、その変数(つまり、コンストラクタ)を使用して新しい Person インスタンスを作成しています。

※ TypeScript ではクラスとそのコンストラクタは密接に関連していて、互換性のある型として扱うことができます。クラスコンストラクタ関数の型はクラスオブジェクトの型と言えます。

以下は少し具体的な使用例です(1〜10行目までは同じ)。

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

type PersonCtor = new (name:string, age:number) => Person;

// 第1引数に PersonCtor 型のコンストラクタ関数を受け取る
function createPerson(ctor: PersonCtor, name: string, age: number): Person {
  return new ctor(name, age);
}

// 呼び出し時には第1引数に Person クラス(のコンストラクタ)を渡す
const person = createPerson(Person, 'John Doe', 30);
console.log(person.name, person.age); // John Doe 30

上記の関数 createPerson は、PersonCtor 型のコンストラクタ(この場合は、Person クラスのコンストラクタ)とコンストラクタに渡す値を受け取り、そのコンストラクタを使用して新しい Person インスタンスを作成して返します(一種のファクトリパターン)。

呼び出し時には、関数 createPerson の第1引数として Person クラス(つまり、そのコンストラクタ)を渡すことができます。なぜなら、Person クラスは PersonCtor 型(つまり、特定のシグネチャを持つコンストラクタ関数の型)と互換性があるからです。

コンストラクタ関数の型は上記の他に以下のように記述することができます。

type PersonCtor = {
  new (name:string, age:number) : Person;
}

interface を使って記述することもできます。

interface PersonCtor {
  new (name:string, age:number) : Person;
}

instanceof 演算子

あるブジェクトが特定のクラスのインスタンスかどうかは、instanceof 演算子を使って判定できます。

instanceof 演算子は、値 instanceof クラス のような構文を使って、特定の値(オブジェクト)が指定したクラスのインスタンスかをチェックする JavaScript の演算子です。

値が指定されたオブジェクトのインスタンスであれば true を、そうでなければ false を返します。

instanceof は継承関係をチェックすることもできます。

以下の場合、foo は Person クラスのインスタンスなので12行目は true になり、Array のインスタンスではないので13行目は false になります。

また、全てのクラスは Object クラスを継承するので14行目は true になります。

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

const foo = new Person('Foo', 22);

console.log(foo instanceof Person);  // true
console.log(foo instanceof Array);  // false
console.log(foo instanceof Object);  // true(全てのクラスは Object を継承)

const bar: Person = {
name: 'Bar',
age: 7
}

console.log(bar instanceof Person);  // false

最後の行の変数 bar は Person 型ですが、false になります。instanceof はクラスのインスタンスかどうか、言い換えるとそのクラスのコンストラクタで生成されたかどうかを判定します。

この例の bar の場合、Person 型ですが、new Person() で生成されていないので false になります。

以下は Person 型の値を引数にとり、その値が Person クラスのインスタンスであれば「Instance of Person」と出力し、そうでなければ「Not instance of Person」と出力する関数の例です。

function isInstanceOfPerson(person: Person): void {
  if(person instanceof Person) {
    console.log('Instance of Person');
  }else{
    console.log('Not instance of Person');
  }
}

isInstanceOfPerson(foo);  // Instance of Person
isInstanceOfPerson(bar);  // Not instance of Person

このように、instanceof を使った判定は、クラスの型を判定するわけではないので注意が必要です。

クラスの継承

あるクラスの構造や機能を引き継いだ新しいクラスを定義することを継承と呼び、extends キーワードを使うことで既存のクラスを継承できます。

継承元となるクラスを親クラスや基底クラス、スーパークラス、継承したクラスを子クラスや派生クラス、サブクラスなどと呼びます。

親クラスを継承して作成した子クラスのインスタンスは、親クラスのインスタンスの代わりに使用することができ、子クラスのインスタンスの型は親クラスのインスタンスの型の部分型になります。

class 構文のクラス名の後に extends キーワードで継承元となる親クラス(基底クラス)を指定します。

class クラス名 extends 親クラス {
}

以下は Person クラスを継承して User というクラスを定義する例です。

Person クラスを継承した User クラスは Person クラスの構造や機能(プロパティ、メソッド、コンストラクタ)を全て引き継ぎます。また、追加で定義したプロパティ utype を持っています。

User クラスの宣言ではコンストラクタを省略しているので Person クラスのコンストラクタが適用されるます。そのため User クラスのインスタンスを生成するには Person クラス同様、コンストラクタの引数に文字列と数値を渡します。

// 継承元となる親クラス
class Person {
  name: string;
  age: number;

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

  greet(): string {
    return `Hello, my name is ${this.name}.`;
  }
}

// Person クラスを継承した User クラス
class User extends Person {
  utype: string = 'basic';
}

// User クラスのインスタンス化
const foo = new User('Foo', 22);

console.log(foo.name); // Foo(継承したプロパティ)
console.log(foo.age);; // 22(継承したプロパティ)
console.log(foo.greet());; // Hello, my name is Foo.(継承したメソッド)
console.log(foo.utype); // basic(User クラスで定義したプロパティ)

User は Person の機能を全て持っているので User 型は Person 型の部分型になります。

そのため、以下のように、Person 型が必要なところに User 型のオブジェクトを渡すことができます。

// Person 型の引数を受け取る関数
function showAge(p: Person) {
  console.log(`I'am ${p.age} years old.`);
}

const bar = new Person('Bar', 33);
showAge(bar);  // I'am 33 years old.

const baz = new User('Baz', 15);
// Person 型の引数に User 型を渡すことができる
showAge(baz);  // I'am 15 years old.

このように継承を使うことで、あるクラスの部分型となるクラスを作成することができます。

オーバーライド(親機能の上書き)

子クラスでは、親の機能を上書き(オーバーライド)することができます。

親の機能を上書きするには、親が持つ機能を子クラスで再宣言します(同じ名前で定義します)。

class Person {
  name: string;
  age: number;

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

  greet(): string {
    return `Hello, my name is ${this.name}.`;
  }
}

class User extends Person {
  utype: string = 'basic';

  // 同じ名前で定義して上書き
  greet(): string {
    return `Hi, I'm ${this.utype} user.`;
  }
}

const bar = new Person('Bar', 33);
console.log(bar.greet());  // Hello, my name is Bar.

const baz = new User('Baz', 15);
console.log(baz.greet());  // Hi, I'm basic user.

オーバーライドの条件

TypeScript では親の機能をオーバーライドする場合、子クラスのインスタンスは親クラスのインスタンスの部分型であるので、その条件を守る必要があります。

例えば、Person クラスの greet() メソッドは () => string 型なので、User クラスで上書きする場合も同じく () => string 型にする必要があります。

もし、以下のように User クラスで greet() メソッドを上書きする際に、戻り値の型を string 型以外にするとコンパイルエラーになります。

class User extends Person {
  utype: string = 'basic';

  // 戻り値の型が異なるので異なるのでコンパイルエラー
  greet(): void {
    // Property 'greet' in type 'User' is not assignable to the same property in base type 'Person'. Type '() => void' is not assignable to type '() => string'.
    console.log( `Hi, I'm ${this.utype} user.`)
  }
}

同様に、プロパティを再宣言する際に異なる型に変更するとコンパイルエラーになります。

super プロパティ

メソッドをオーバーライドした場合、その処理は子クラスのメソッド内の処理に置き換わります。

親クラスのメソッドの処理を利用したい場合、子クラスのメソッドからは、super.プロパティ名で親クラスのメソッドを参照できます。

class Person {
  name: string;
  age: number;

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

  greet(): string {
    return `Hello, my name is ${this.name}.`;
  }
}

class User extends Person {
  utype: string = 'basic';

  greet(): string {
    // 親クラスのメソッドを参照
    const greeing = super.greet();
    return greeing + ` I'm ${this.utype} user.`;
  }
}
const foo = new User('Foo', 22);
console.log(foo.greet());  // Hello, my name is Foo. I'm basic user.
コンストラクタのオーバーライド

コンストラクタをオーバライドすることもできます。

子クラスでコンストラクタを使う(上書きする)場合は、子クラスのコンストラクタの中で super() を呼び出す必要があります。super() は子クラスから親クラスの constructor メソッドを呼び出します。

また、コンストラクタの中では this を使う前に super() を呼び出す必要があります(先に super() を呼び出して親クラスのコンストラクタ処理を実行してからでないと this を参照することができないため)。

以下は子クラスの User でコンストラクタの引数を増やして utype プロパティの初期値も指定できるようにする例です。

class Person {
  name: string;
  age: number;

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

  greet(): string {
    return `Hello, my name is ${this.name}.`;
  }
}

class User extends Person {
  utype: string;

  // コンストラクタをオーバーライド
  constructor(name: string, age: number, utype: string) {
    // this を使う前に super() を呼び出す
    super(name, age);  // 引数は親クラスのコンストラクタの引数の数と型を一致させる
    this.utype = utype;
  }
}

const foo = new User('Foo', 22, 'advanced');
console.log(foo.utype); // advanced

TypeScriptでは super() に指定する引数は、親クラスのコンストラクタの引数の数及び型と一致させる必要があります。

super() に指定する引数は数と型が一致していれば、どのような値を指定するかは子クラスの自由です。以下のような User20 という全ての age が 20 であるクラスを作成することもできます。

以下の User20 クラスはコンストラクタに2つの引数を取り、super() の第2引数には固定の数値 20 を渡しています。

class User20 extends Person {
  utype: string;

  constructor(name: string, utype: string) {
    super(name, 20);
    this.utype = utype;
  }
}

const bar = new User20('Foo', '20 years old');
console.log(bar.age);  // 20
override 修飾子

TypeScript には override というキーワードがあり、クラス内のプロパティやメソッドに修飾子として付けることで、それらがオーバーライドであることを明示することができます(JavaScript にはありません)。

以下は子クラスでオーバーライドしているメソッド hello() に override 修飾子を指定しています。

※ オーバーライドでないものに override 修飾子を指定すると、コンパイルエラーになります。

class Parent {
  hello(): void {
    console.log('hello from parent.');
  }
}
class Child extends Parent {
  // override を指定してオーバーライドであることを明示
  override hello(): void {
    console.log('hello from child.');
  }
}

override 修飾子をつけた状態で親クラスのメソッドを削除すると、コンパイルエラーになります。

これにより、リファクタリングなどで親クラスでメソッドが削除されたことを検知することができます。

class Parent { //メソッドを削除 }

class Child extends Parent {
  override hello(): void { // コンパイルエラー
    // This member cannot have an 'override' modifier because it is not declared in the base class 'Parent'(Parent に対応するメソッドがないので override は指定できない)
    console.log('hello from child.');
  }
}

noImplicitOverride

override 修飾子はコンパイラオプションの noImplicitOverride と組み合わせると効果的です。

noImplicitOverride を有効にすると、子クラスが親クラスのメソッドをオーバーライドするときに override 修飾子を指定することを必須にします(noImplicitOverride はデフォルトでは無効)。

tsconfig.json で "noImplicitOverride": true を指定して有効にした場合、以下のように子クラスで override 修飾子を指定せずにメソッドをオーバーライドするとコンパイルエラーになります。

class Parent {
  hello(): void {
    console.log('hello from parent.');
  }
}
class Child extends Parent {
  // noImplicitOverride を有効にすると、override を指定していないとコンパイルエラー
  hello(): void {
    // This member must have an 'override' modifier because it overrides a member in the base class 'Parent'.
    console.log('hello from child.');
  }
}

これにより、オーバーライドしたつもりができていない場合やオーバーライドするつもりがないのにオーバーライドしてしまった場合にコンパイルエラーにより気付くことができます。

private / protected

アクセス修飾子private やプロパティ名の前に # を付けるプライベートプロパティは、継承の場合の子クラスからでもアクセスできません。

例えば、以下のように子クラスで private 修飾子や # の付いたプライベートなプロパティにアクセスしようとするとエラーになります。

class Parent {
  private name: string;  // private アクセス修飾子
  #age: number;  // プライベートプロパティ

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

class Child extends Parent {
  printName(): void {
    console.log(this.name);  // コンパイルエラー
    // Property 'name' is private and only accessible within class 'Parent'.
  }
  printAge(): void {
    console.log(this.#age);  // コンパイルエラーと Uncaught SyntaxError
    // Property '#age' is not accessible outside class 'Parent' because it has a private identifier.
  }
}

子クラスからは利用させたいが、外部に公開したくない場合には以下のように protected を使うことができます。protected を利用すると、子クラスである Child からはアクセスできるようになりますが、外部(クラス外)からアクセスするとコンパイルエラーになります。

class Parent {
  protected name: string;
  protected age: number;

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

class Child extends Parent {
  printName(): void {
    console.log(this.name);
  }
  printAge(): void {
    console.log(this.age);
  }
}

const foo = new Child('foo',23);
foo.printName();  // foo
foo.printAge();  // 23
console.log(foo.name); // クラス外からアクセスするとコンパイルエラー
console.log(foo.age); // クラス外からアクセスするとコンパイルエラー

継承時にアクセス修飾子を変更

クラスの継承時にアクセス制限を緩める方向(protected から public)にであれば、アクセス修飾子を変更することができます。

例えば、以下のように protected から public に変更することはできます。

class Parent {
  protected name = 'foo';
  protected hello(): void {
    console.log("Hello!" + this.name);
  }
}

class Child extends Parent {
  public name = 'bar';  // public に変更(OK)
  public hello(): void {  // public に変更(OK)
    console.log("Hello!" + this.name);
  }
}

但し、逆方向(public から protected)への変更はできません。また、private から public への変更もできません。

implements によるクラスの型チェック

クラスを作成する際に、implements キーワードを使ってクラスが特定の型を満たしていることを示す(チェックする)ことができます。

implements キーワードは、TypeScript において特定のクラスが特定の型を実装していることを明示的に指定するために使用することができます。これにより、コンパイラがコードを型チェックでき、実装が正しく行われているかを確認できます。

以下が implements キーワードを使ったクラスの構文になります。

これはそのクラスのインスタンスは与えられた型の部分型であるということを示しており、言い換えるとそのクラスは implements で指定された型の全てのプロパティやメソッドを実装する必要があります。

class クラス名 implements 型 {
  ...
}

以下は Person 型(Person クラスの型)が PersonType 型の部分型であることを宣言しています。

Person クラスが PersonType を implements している場合、Person クラスが PersonType で定義されたすべてのプロパティを実装しているので、Person クラスは PersonType の部分型と見なされます。

そのため、Person クラスのインスタンスを PersonType 型の変数に代入できます。

// 実装する型(型エイリアスを使用したプロパティの指定)
type PersonType = {
  name: string;
  age: number;
};

// Person クラスが PersonType 型のプロパティを実装する
class Person implements PersonType {
  name: string;
  age: number;

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

// Person クラスのインスタンスを PersonType 型の変数に代入
const person: PersonType = new Person('Foo', 25);

console.log(person.name); // Foo
console.log(person.age);  // 25

Person の定義から age プロパティを削除すると「クラス 'Person' はインターフェイス 'PersonType' を正しく実装していません。プロパティ 'age' は型 'Person' にありませんが、型 'PersonType' では必須です。」という内容のコンパイルエラーになります。

type PersonType = {
  name: string;
  age: number;
};

class Person implements PersonType { // コンパイルエラー
  // Class 'Person' incorrectly implements interface 'PersonType'.
  // Property 'age' is missing in type 'Person' but required in type 'PersonType'.
  name: string;

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

implements キーワードではインターフェースも指定できます。以下は実装する型をインターフェース(interface キーワード)を使用して定義する例です。

// インターフェースの定義(実装する型)
interface PersonType {
  name: string;
  age: number;
}

// Person クラスが PersonType インターフェースを実装する
class Person implements PersonType {
  name: string;
  age: number;

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

// Person クラスのインスタンスを PersonType 型の変数に代入
const person: PersonType = new Person('Foo', 25);

以下の例では、Circle と Square クラスが number を返す getArea() メソッドのメソッドシグネチャを持つ Shape 型を実装しています。

また、関数 logArea には Shape 型の引数が期待されています。

そのため、Circle および Square のインスタンスは logArea 関数に渡すことができますが、Rectangle のインスタンスは Shape を実装していないためエラーとなります。

// 実装する型(型エイリアスを使用したメソッドの指定)
type Shape = {
  getArea(): number;  // メソッドシグネチャ
};

/* //インターフェースで Shape を定義する場合
interface Shape {
  getArea(): number;
}; */

// Circle クラスが Shape 型を実装する
class Circle implements Shape {
  radius: number;
  constructor(radius: number) {
    this.radius = radius;
  }
  getArea(): number {
    return Math.PI * this.radius ** 2; // **(べき乗演算子)
  }
}

// Square クラスが Shape 型を実装する
class Square implements Shape {
  sideLength: number;
  constructor(sideLength: number) {
    this.sideLength = sideLength;
  }
  getArea(): number {
    return this.sideLength ** 2;
  }
}

// Rectangle クラスは Shape 型を実装しない
class Rectangle {
  width: number;
  height: number;
  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }
}

// 型エイリアスを実装したクラスのインスタンスを作成
const circle = new Circle(5);  // const circle: Shape = new Circle(5);
const square = new Square(4);  // const square: Shape = new Square(4);

// Rectangle クラスのインスタンスを作成
const rectangle = new Rectangle(3, 6);

// Shape 型の引数を受け取る関数
function logArea(shape: Shape) {
  console.log(`Area: ${shape.getArea()}`);
}

logArea(circle);   // OK
logArea(square);   // OK
logArea(rectangle); // エラー(Rectangle は Shape を実装していないため以下のエラー)
// Argument of type 'Rectangle' is not assignable to parameter of type 'Shape'.
// Property 'getArea' is missing in type 'Rectangle' but required in type 'Shape'.
// Uncaught TypeError: shape.getArea is not a function(JavaScript のエラー)

部分型関係を作るために implements は必須ではない

TypeScript では構造的部分型を採用しているので、部分型関係を作るのに implements は必須ではありません。上記の例の場合、number 型の戻り値を返すメソッド getArea() を実装していれば、そのクラスは Shape の部分型になります。

上記の場合、Circle と Square のクラス定義から implements Shape を削除しても機能しますが、implements Shape を書くことにより、クラス定義において Circle や Square が Shape の部分型であることをチェックすることができます。

例えば、上記の Circle クラスの定義で getArea() を削除すれば定義部分がコンパイルエラーになりますが、implements Shape の記述がなければ定義部分ではエラーにならず、実行時にエラーになります。