著者はCOVID-19救済基金を選択し、 Write forDOnationsプログラムの一環として寄付を受け取りました。

序章

クラスは、オブジェクト指向プログラミング(OOP)言語で使用される一般的な抽象化であり、オブジェクトと呼ばれるデータ構造を記述します。 これらのオブジェクトには初期状態が含まれ、その特定のオブジェクトインスタンスにバインドされた動作を実装する場合があります。 2015年、 ECMAScript 6 は、 JavaScript に新しい構文を導入して、言語のプロトタイプ機能を内部的に使用するクラスを作成しました。 TypeScriptはその構文を完全にサポートしており、メンバーの可視性、抽象クラス、ジェネリッククラス、矢印関数メソッドなどの機能も追加しています。

このチュートリアルでは、クラスの作成に使用される構文、使用可能なさまざまな機能、およびコンパイル時の型チェック中にTypeScriptでクラスがどのように扱われるかについて説明します。 さまざまなコードサンプルを使用した例を紹介します。これは、独自のTypeScript環境で実行できます。

前提条件

このチュートリアルに従うには:

  • TypeScriptプログラムを実行して、例に従うことができる環境。 これをローカルマシンに設定するには、次のものが必要です。 TypeScript関連のパッケージを処理する開発環境を実行するためにインストールされたNodeとnpm(またはyarn)の両方。 このチュートリアルは、Node.jsバージョン14.3.0およびnpmバージョン6.14.5でテストされました。 macOSまたはUbuntu18.04にインストールするには、「Node.jsをインストールしてmacOSにローカル開発環境を作成する方法」または「Ubuntu18.04にNode.jsをインストールする方法」の「PPAを使用したインストール」セクションの手順に従います。 これは、Windows Subsystem for Linux(WSL)を使用している場合にも機能します。 さらに、TypeScriptコンパイラ(tsc)がマシンにインストールされている必要があります。 これを行うには、TypeScriptの公式Webサイトを参照してください。
  • ローカルマシン上にTypeScript環境を作成したくない場合は、公式の TypeScriptPlaygroundを使用してフォローできます。
  • JavaScript、特に destructuring、rest演算子 imports /exportsなどのES6+構文に関する十分な知識が必要です。 これらのトピックに関する詳細情報が必要な場合は、JavaScriptシリーズのコーディング方法を読むことをお勧めします。
  • このチュートリアルでは、TypeScriptをサポートし、インラインエラーを表示するテキストエディタの側面を参照します。 これはTypeScriptを使用するために必要ではありませんが、TypeScript機能をさらに活用します。 これらの利点を活用するには、 Visual Studio Code のようなテキストエディターを使用できます。このエディターは、TypeScriptをすぐにサポートします。 TypeScriptPlaygroundでこれらの利点を試すこともできます。

このチュートリアルに示されているすべての例は、TypeScriptバージョン4.3.2を使用して作成されています。

TypeScriptでクラスを作成する

このセクションでは、TypeScriptでクラスを作成するために使用される構文の例を実行します。 TypeScriptを使用してクラスを作成する基本的な側面のいくつかについて説明しますが、構文は、JavaScriptを使用してクラスを作成するために使用されるものとほとんど同じです。 このため、このチュートリアルでは、TypeScriptで使用できるいくつかの特徴的な機能に焦点を当てます。

次のコードに示すように、classキーワード、クラス名、{}ペアブロックの順に使用して、クラス宣言を作成できます。

class Person {

}

このスニペットは、Personという名前の新しいクラスを作成します。 次に、newキーワード、クラス名、空のパラメータリスト(省略可能)を使用して、Personクラスの新しいインスタンスを作成できます。 )、次の強調表示されたコードに示されているように:

class Person {

}

const personInstance = new Person();

クラス自体は、指定された形状のオブジェクトを作成するための青写真と考えることができますが、インスタンスは、この青写真から作成されたオブジェクト自体です。

クラスを操作する場合、ほとんどの場合、constructor関数を作成する必要があります。 constructorは、クラスの新しいインスタンスが作成されるたびに実行されるメソッドです。 これは、クラスの値を初期化するために使用できます。

Personクラスにコンストラクターを導入します。

class Person {
  constructor() {
    console.log("Constructor called");
  }
}

const personInstance = new Person();

このコンストラクターは、personInstanceが作成されると、Constructor calledをコンソールに記録します。

コンストラクターは、パラメーターを受け入れるという点で通常の関数に似ています。 これらのパラメーターは、クラスの新しいインスタンスを作成するときにコンストラクターに渡されます。 現在、クラスのインスタンスを作成するときに空のパラメーターリスト()で示されているように、コンストラクターにパラメーターを渡していません。

次に、タイプstringnameという新しいパラメーターを導入します。

class Person {
  constructor(name: string) {
    console.log(`Constructor called with name=${name}`);
  }
}

const personInstance = new Person("Jane");

強調表示されたコードで、タイプstringnameというパラメーターをクラスコンストラクターに追加しました。 次に、Personクラスの新しいインスタンスを作成するときに、そのパラメーターの値(この場合は文字列"Jane")も設定します。 最後に、console.logを変更して、引数を画面に出力しました。

このコードを実行すると、ターミナルに次の出力が表示されます。

Output
Constructor called with name=Jane

ここでは、コンストラクターのパラメーターはオプションではありません。 これは、クラスをインスタンス化するときに、nameパラメーターをコンストラクターに渡す必要があることを意味します。 次の例のように、nameパラメーターをコンストラクターに渡さない場合。

const unknownPerson = new Person;

TypeScriptコンパイラはエラー2554を出します:

Output
Expected 1 arguments, but got 0. (2554) filename.ts(4, 15): An argument for 'name' was not provided.

TypeScriptでクラスを宣言したので、次にプロパティを追加してそれらのクラスを操作します。

クラスプロパティの追加

クラスの最も有用な側面の1つは、クラスから作成された各インスタンスの内部にあるデータを保持する機能です。 これは、プロパティを使用して行われます。

TypeScriptには、このプロセスをJavaScriptクラスと区別するいくつかの安全性チェックがあります。たとえば、プロパティを初期化してundefinedにならないようにする必要があります。 このセクションでは、これらの安全性チェックを説明するために、クラスに新しいプロパティを追加します。

TypeScriptでは、通常、クラスの本体で最初にプロパティを宣言し、それに型を指定する必要があります。 たとえば、nameプロパティをPersonクラスに追加します。

class Person {
  name: string;

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

この例では、constructorでプロパティを設定することに加えて、タイプstringでプロパティnameを宣言します。

注: TypeScriptでは、クラス内のプロパティの可視性を宣言して、データにアクセスできる場所を決定することもできます。 name: string宣言では、可視性は宣言されていません。つまり、プロパティは、どこからでもアクセスできるデフォルトのpublicステータスを使用します。 可視性を明示的に制御したい場合は、プロパティでこれを宣言します。 これについては、チュートリアルの後半で詳しく説明します。

プロパティにデフォルト値を指定することもできます。 例として、instantiatedAtという新しいプロパティを追加します。このプロパティは、クラスインスタンスがインスタンス化された時刻に設定されます。

class Person {
  name: string;
  instantiatedAt = new Date();

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

これは、 Dateオブジェクトを使用して、インスタンスの作成の初期日付を設定します。 このコードが機能するのは、次のように、クラスコンストラクターが呼び出されたときにデフォルト値のコードが実行されるためです。これは、コンストラクターに値を設定するのと同じです。

class Person {
  name: string;
  instantiatedAt: Date;

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

クラスの本体でデフォルト値を宣言することにより、コンストラクターで値を設定する必要はありません。

クラス内のプロパティにタイプを設定する場合は、そのプロパティもそのタイプの値に初期化する必要があることに注意してください。 これを説明するために、次のコードのように、クラスプロパティを宣言しますが、初期化子を提供しないでください。

class Person {
  name: string;
  instantiatedAt: Date;

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

instantiatedAtにはDateのタイプが割り当てられているため、常にDateオブジェクトである必要があります。 ただし、初期化がないため、クラスがインスタンス化されると、プロパティはundefinedになります。 このため、TypeScriptコンパイラはエラー2564を表示します。

Output
Property 'instantiatedAt' has no initializer and is not definitely assigned in the constructor. (2564)

これは、クラスのインスタンス化時に正しいプロパティが存在することを確認するための追加のTypeScript安全性チェックです。

TypeScriptには、コンストラクターに渡されるパラメーターと同じ名前のプロパティを作成するためのショートカットもあります。 このショートカットは、パラメータプロパティと呼ばれます。

前の例では、nameプロパティを、クラスコンストラクターに渡されたnameパラメーターの値に設定しました。 クラスにフィールドを追加すると、これを書くのが面倒になる可能性があります。 たとえば、タイプnumberageという新しいフィールドをPersonクラスに追加し、コンストラクターにも追加します。

class Person {
  name: string;
  age: number;
  instantiatedAt = new Date();

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

これは機能しますが、TypeScriptは、パラメータープロパティ、またはコンストラクターのパラメーターに設定されたプロパティを使用して、このような定型コードを減らすことができます。

class Person {
  instantiatedAt = new Date();

  constructor(
    public name: string,
    public age: number
  ) {}
}

このスニペットでは、nameおよびageプロパティ宣言をクラス本体から削除し、コンストラクターのパラメーターリスト内に移動しました。 これを行うと、これらのコンストラクターパラメーターもそのクラスのプロパティであることがTypeScriptに通知されます。 このように、以前のように、クラスのプロパティをコンストラクターで受け取ったパラメーターの値に設定する必要はありません。

注:可視性修飾子publicがコードに明示的に記述されていることに注意してください。 この修飾子は、パラメータープロパティを設定するときに含める必要があり、publicの可視性に自動的にデフォルト設定されることはありません。

TypeScriptコンパイラによって発行されたコンパイル済みJavaScriptを見ると、このコードは次のJavaScriptコードにコンパイルされます。

"use strict";
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
    this.instantiatedAt = new Date();
  }
}

これは、元の例がコンパイルされるのと同じJavaScriptコードです。

TypeScriptクラスのプロパティの設定を試したので、クラス継承を使用してクラスを新しいクラスに拡張することに進むことができます。

TypeScriptでのクラス継承

TypeScriptは、JavaScriptのクラス継承の全機能を提供し、インターフェイス抽象クラスの2つの主な追加機能があります。 インターフェイスは、より複雑なデータの型チェックを提供するように、クラスまたはオブジェクトの形状を記述および適用する構造です。 クラスにインターフェースを実装して、それが特定のパブリックシェイプを持っていることを確認できます。 抽象クラスは、他のクラスの基礎として機能するクラスですが、それ自体をインスタンス化することはできません。 これらは両方とも、クラス継承を介して実装されます。

このセクションでは、インターフェイスと抽象クラスを使用してクラスの型チェックを構築および作成する方法の例をいくつか紹介します。

インターフェイスの実装

インターフェイスは、そのインターフェイスのすべての実装が持つ必要のある一連の動作を指定するのに役立ちます。 インターフェイスは、interfaceキーワード、インターフェイスの名前、インターフェイス本体の順に使用して作成されます。 例として、プログラムの実行方法に関する重要なデータをログに記録するために使用できるLoggerインターフェイスを作成します。

interface Logger {}

次に、インターフェースに4つのメソッドを追加します。

interface Logger {
  debug(message: string, metadata?: Record<string, unknown>): void;
  info(message: string, metadata?: Record<string, unknown>): void;
  warning(message: string, metadata?: Record<string, unknown>): void;
  error(message: string, metadata?: Record<string, unknown>): void;
}

このコードブロックに示されているように、インターフェイスでメソッドを作成するときは、実装を追加せず、タイプ情報のみを追加します。 この場合、debuginfowarning、およびerrorの4つの方法があります。 それらはすべて同じ型署名を共有します。2つのパラメーター、タイプstringmessageと、タイプRecord<string, unknown>のオプションのmetadataパラメーターを受け取ります。 それらはすべてvoidタイプを返します。

このインターフェースを実装するすべてのクラスには、これらの各メソッドに対応するパラメーターと戻り値のタイプが必要です。 ConsoleLoggerという名前のクラスにインターフェイスを実装します。このクラスは、consoleメソッドを使用してすべてのメッセージをログに記録します。

class ConsoleLogger implements Logger {
  debug(message: string, metadata?: Record<string, unknown>) {
    console.info(`[DEBUG] ${message}`, metadata);
  }
  info(message: string, metadata?: Record<string, unknown>) {
    console.info(message, metadata);
  }
  warning(message: string, metadata?: Record<string, unknown>) {
    console.warn(message, metadata);
  }
  error(message: string, metadata?: Record<string, unknown>) {
    console.error(message, metadata);
  }
}

インターフェイスを作成するときに、implementsという新しいキーワードを使用して、クラスが実装するインターフェイスのリストを指定していることに注意してください。 implementsキーワードの後に、インターフェイスIDのコンマ区切りリストとして追加することにより、複数のインターフェイスを実装できます。 たとえば、Clearableという別のインターフェイスがある場合:

interface Clearable {
  clear(): void;
}

次の強調表示されたコードを追加することで、ConsoleLoggerクラスに実装できます。

class ConsoleLogger implements Logger, Clearable {
  clear() {
    console.clear();
  }
  debug(message: string, metadata?: Record<string, unknown>) {
    console.info(`[DEBUG] ${message}`, metadata);
  }
  info(message: string, metadata?: Record<string, unknown>) {
    console.info(message, metadata);
  }
  warning(message: string, metadata?: Record<string, unknown>) {
    console.warn(message, metadata);
  }
  error(message: string, metadata?: Record<string, unknown>) {
    console.error(message, metadata);
  }
}

クラスが新しいインターフェイスに準拠していることを確認するために、clearメソッドも追加する必要があることに注意してください。

Loggerインターフェイスのdebugメソッドのように、いずれかのインターフェイスに必要なメンバーの1つに実装を提供しなかった場合、TypeScriptコンパイラはエラー[を返します。 X209X]:

Output
Class 'ConsoleLogger' incorrectly implements interface 'Logger'. Property 'debug' is missing in type 'ConsoleLogger' but required in type 'Logger'. (2420)

TypeScriptコンパイラは、実装が実装しているインターフェイスで期待されるものと一致しなかった場合にもエラーを表示します。 たとえば、debugメソッドのmessageパラメータのタイプをstringからnumberに変更すると、エラー2416が発生します。 ]:

Output
Property 'debug' in type 'ConsoleLogger' is not assignable to the same property in base type 'Logger'. Type '(message: number, metadata?: Record<string, unknown> | undefined) => void' is not assignable to type '(message: string, metadata: Record<string, unknown>) => void'. Types of parameters 'message' and 'message' are incompatible. Type 'string' is not assignable to type 'number'. (2416)

抽象クラスに基づいて構築する

抽象クラスは通常のクラスに似ていますが、2つの大きな違いがあります。直接インスタンス化できないことと、抽象メンバーが含まれている可能性があることです。 抽象メンバーは、継承クラスで実装する必要のあるメンバーです。 抽象クラス自体には実装がありません。 これは、基本抽象クラスにいくつかの共通機能を持ち、継承クラスにさらに具体的な実装を含めることができるので便利です。 クラスを抽象としてマークすると、このクラスには、継承するクラスで実装する必要のある機能が不足していると言えます。

抽象クラスを作成するには、強調表示されたコードのように、classキーワードの前にabstractキーワードを追加します。

abstract class AbstractClassName {

}

次に、抽象クラスにメンバーを作成できます。実装がある場合とない場合があります。 実装されていないものはabstractとしてマークされ、抽象クラスから拡張されたクラスに実装する必要があります。

たとえば、 Node.js 環境で作業していて、独自のStream実装を作成しているとします。 そのために、2つの抽象メソッドreadwriteを持つStreamという抽象クラスを作成します。

declare class Buffer {
  from(array: any[]): Buffer;
  copy(target: Buffer, offset?: number): void;
}

abstract class Stream {

  abstract read(count: number): Buffer;
  
  abstract write(data: Buffer): void;
}

ここでのBufferオブジェクトは、バイナリデータを格納するために使用されるNode.jsで利用可能なクラスです。 上部のdeclare class Bufferステートメントを使用すると、TypeScriptPlaygroundのようなNode.js型宣言なしでTypeScript環境でコードをコンパイルできます。

この例では、readメソッドは内部データ構造からバイトをカウントし、Bufferオブジェクトを返し、writeBufferインスタンスのすべてのコンテンツを書き込みますストリームに。 これらのメソッドは両方とも抽象的であり、Streamから拡張されたクラスでのみ実装できます。

次に、実装がある追加のメソッドを作成できます。 このようにして、Stream抽象クラスから拡張するクラスは、これらのメソッドを自動的に受け取ります。 そのような例の1つは、copyメソッドです。

declare class Buffer {
  from(array: any[]): Buffer;
  copy(target: Buffer, offset?: number): void;
}

abstract class Stream {

  abstract read(count: number): Buffer;
  
  abstract write(data: Buffer): void;

  copy(count: number, targetBuffer: Buffer, targetBufferOffset: number) {
    const data = this.read(count);
    data.copy(targetBuffer, targetBufferOffset);
  }
}

このcopyメソッドは、targetBufferOffsetから開始して、ストリームからtargetBufferにバイトを読み取った結果をコピーします。

次に、FileStreamクラスのように、Stream抽象クラスの実装を作成すると、FileStreamクラス:

declare class Buffer {
  from(array: any[]): Buffer;
  copy(target: Buffer, offset?: number): void;
}

abstract class Stream {

  abstract read(count: number): Buffer;
  
  abstract write(data: Buffer): void;

  copy(count: number, targetBuffer: Buffer, targetBufferOffset: number) {
    const data = this.read(count);
    data.copy(targetBuffer, targetBufferOffset);
  }
}

class FileStream extends Stream {
  read(count: number): Buffer {
    // implementation here
    return new Buffer();
  }
  
  write(data: Buffer) {
    // implementation here
  }
}

const fileStream = new FileStream();

この例では、fileStreamインスタンスで自動的にcopyメソッドを使用できます。 FileStreamクラスは、Stream抽象クラスに準拠するために、readおよびwriteメソッドも明示的に実装する必要がありました。

FileStreamクラスにwrite実装を追加しないなど、拡張元の抽象クラスの抽象メンバーの1つを実装するのを忘れた場合、TypeScriptコンパイラはエラー2515

Output
Non-abstract class 'FileStream' does not implement inherited abstract member 'write' from class 'Stream'. (2515)

writeメソッドの最初のパラメーターのタイプを[ではなくstringタイプに変更するなど、メンバーのいずれかを誤って実装した場合にも、TypeScriptコンパイラーはエラーを表示します。 X212X]:

Output
Property 'write' in type 'FileStream' is not assignable to the same property in base type 'Stream'. Type '(data: string) => void' is not assignable to type '(data: Buffer) => void'. Types of parameters 'data' and 'data' are incompatible. Type 'Buffer' is not assignable to type 'string'. (2416)

抽象クラスとインターフェースを使用すると、クラスのより複雑な型チェックをまとめて、基本クラスから拡張されたクラスが正しい機能を継承していることを確認できます。 次に、TypeScriptでメソッドとプロパティの可視性がどのように機能するかの例を実行します。

クラスメンバーの可視性

TypeScriptは、クラスのメンバーの可視性を指定できるようにすることで、使用可能なJavaScriptクラス構文を拡張します。 この場合、 visibility は、インスタンス化されたクラスの外部のコードがクラス内のメンバーとどのように相互作用できるかを示します。

TypeScriptのクラスメンバーには、publicprotected、およびprivateの3つの可視性修飾子があります。 publicメンバーは、クラスインスタンスの外部からアクセスできますが、privateメンバーはアクセスできません。 protectedは、2つの中間点を占め、クラスのインスタンスまたはそのクラスに基づくサブクラスからメンバーにアクセスできます。

このセクションでは、使用可能な可視性修飾子を調べて、それらの意味を学習します。

public

これは、TypeScriptのクラスメンバーのデフォルトの可視性です。 クラスメンバーに可視性修飾子を追加しない場合は、publicに設定するのと同じです。 パブリッククラスのメンバーは、制限なしでどこからでもアクセスできます。

これを説明するために、前のPersonクラスに戻ります。

class Person {
  public instantiatedAt = new Date();

  constructor(
    name: string,
    age: number
  ) {}
}

このチュートリアルでは、2つのプロパティnameageがデフォルトでpublicの可視性を持っていることを説明しました。 型の可視性を明示的に宣言するには、プロパティの前にpublicキーワードを追加し、getBirthYearという新しいpublicメソッドをクラスに追加します。これにより、Personインスタンス:

class Person {
  constructor(
    public name: string,
    public age: number
  ) {}

  public getBirthYear() {
    return new Date().getFullYear() - this.age;
  }
}

次に、クラスインスタンスの外部のグローバルスペースでプロパティとメソッドを使用できます。

class Person {
  constructor(
    public name: string,
    public age: number
  ) {}

  public getBirthYear() {
    return new Date().getFullYear() - this.age;
  }
}

const jon = new Person("Jon", 35);

console.log(jon.name);
console.log(jon.age);
console.log(jon.getBirthYear());

このコードは、コンソールに次のように出力します。

Output
Jon 35 1986

クラスのすべてのメンバーにアクセスできることに注意してください。

protected

protectedの可視性を持つクラスメンバーは、そのクラス内またはそのクラスのサブクラスで宣言されているクラス内でのみ使用できます。

次のEmployeeクラスとそれに基づくFinanceEmployeeクラスを見てください。

class Employee {
  constructor(
    protected identifier: string
  ) {}
}

class FinanceEmployee extends Employee {
  getFinanceIdentifier() {
    return `fin-${this.identifier}`;
  }
}

強調表示されたコードは、protectedの可視性で宣言されたidentifierプロパティを示しています。 this.identifierコードは、FinanceEmployeeサブクラスからこのプロパティにアクセスしようとします。 このコードは、TypeScriptでエラーなしで実行されます。

次の例のように、クラス自体またはサブクラス内にない場所からそのメソッドを使用しようとした場合:

class Employee {
  constructor(
    protected identifier: string
  ) {}
}

class FinanceEmployee extends Employee {
  getFinanceIdentifier() {
    return `fin-${this.identifier}`;
  }
}

const financeEmployee = new FinanceEmployee('abc-12345');
financeEmployee.identifier;

TypeScriptコンパイラはエラー2445を出します:

Output
Property 'identifier' is protected and only accessible within class 'Employee' and its subclasses. (2445)

これは、新しいfinanceEmployeeインスタンスのidentifierプロパティをグローバルスペースから取得できないためです。 代わりに、内部メソッドgetFinanceIdentifierを使用して、identifierプロパティを含む文字列を返す必要があります。

class Employee {
  constructor(
    protected identifier: string
  ) {}
}

class FinanceEmployee extends Employee {
  getFinanceIdentifier() {
    return `fin-${this.identifier}`;
  }
}

const financeEmployee = new FinanceEmployee('abc-12345');
console.log(financeEmployee.getFinanceIdentifier())

これにより、以下がコンソールに記録されます。

Output
fin-abc-12345

private

プライベートメンバーは、それらを宣言するクラス内でのみアクセスできます。 これは、サブクラスでさえアクセスできないことを意味します。

前の例を使用して、Employeeクラスのidentifierプロパティをprivateプロパティに変換します。

class Employee {
  constructor(
    private identifier: string
  ) {}
}

class FinanceEmployee extends Employee {
  getFinanceIdentifier() {
    return `fin-${this.identifier}`;
  }
}

このコードにより、TypeScriptコンパイラはエラー2341を表示します。

Output
Property 'identifier' is private and only accessible within class 'Employee'. (2341)

これは、FinanceEmployeeサブクラスのプロパティidentifierにアクセスしているために発生しますが、identifierプロパティがEmployeeクラスで宣言されているため、これは許可されていません。可視性はprivateに設定されています。

TypeScriptは生のJavaScriptにコンパイルされており、それ自体にはクラスのメンバーの可視性を指定する方法がないことに注意してください。 そのため、TypeScriptには、実行時のそのような使用に対する保護はありません。 これは、コンパイル中にのみTypeScriptコンパイラによって実行される安全性チェックです。

可視性修飾子を試したので、TypeScriptクラスのメソッドとして矢印関数に進むことができます。

矢印関数としてのクラスメソッド

JavaScriptでは、関数のコンテキストを表す this 値は、関数の呼び出し方法に応じて変わる可能性があります。 この変動性は、複雑なコードで混乱を招く場合があります。 TypeScriptを使用する場合、クラスメソッドを作成するときに特別な構文を使用して、thisがクラスインスタンス以外のものにバインドされないようにすることができます。 このセクションでは、この構文を試してみます。

Employeeクラスを使用して、従業員IDの取得にのみ使用される新しいメソッドを導入します。

class Employee {
  constructor(
    protected identifier: string
  ) {}

  getIdentifier() {
    return this.identifier;
  }
}

これは、メソッドを直接呼び出す場合に非常にうまく機能します。

class Employee {
  constructor(
    protected identifier: string
  ) {}

  getIdentifier() {
    return this.identifier;
  }
}

const employee = new Employee("abc-123");

console.log(employee.getIdentifier());

これにより、コンソールの出力に次のように出力されます。

Output
abc-123

ただし、次のコードのように、後で呼び出すためにgetIdentifierインスタンスメソッドをどこかに保存した場合。

class Employee {
  constructor(
    protected identifier: string
  ) {}

  getIdentifier() {
    return this.identifier;
  }
}

const employee = new Employee("abc-123");

const obj = {
  getId: employee.getIdentifier
}

console.log(obj.getId());

値にアクセスできなくなります。

Output
undefined

これは、obj.getId()を呼び出すと、employee.getIdentifier内のthisEmployeeではなくobjオブジェクトにバインドされるためです。実例。

これを回避するには、getIdentifier矢印関数に変更します。 次のコードで強調表示されている変更を確認してください。

class Employee {
  constructor(
    protected identifier: string
  ) {}

  getIdentifier = () => {
    return this.identifier;
  }
}
...

以前と同じようにobj.getId()を呼び出そうとすると、コンソールに次のように正しく表示されます。

Output
abc-123

これは、TypeScriptで矢印関数をクラスメソッドの直接値として使用する方法を示しています。 次のセクションでは、TypeScriptのタイプチェックを使用してクラスを適用する方法を学習します。

タイプとしてのクラスの使用

これまでのところ、このチュートリアルでは、クラスを作成して直接使用する方法について説明してきました。 このセクションでは、TypeScriptを使用するときに、クラスを型として使用します。

クラスはTypeScriptではタイプと値の両方であるため、両方の方法で使用できます。 クラスを型として使用するには、TypeScriptが型を予期する任意の場所でクラス名を使用します。 たとえば、前に作成したEmployeeクラスがあるとします。

class Employee {
  constructor(
    public identifier: string
  ) {}
}

従業員の識別子を出力する関数を作成したいとします。 次のような関数を作成できます。

class Employee {
  constructor(
    public identifier: string
  ) {}
}

function printEmployeeIdentifier(employee: Employee) {
  console.log(employee.identifier);
}

employeeパラメーターをEmployeeタイプに設定していることに注意してください。これは、クラスの正確な名前です。

TypeScriptのクラスは、他のタイプがTypeScriptで比較されるのと同じように、他のクラスを含む他のタイプと構造的に比較されます。 つまり、両方が同じ形状の2つの異なるクラス(つまり、同じ可視性を持つ同じメンバーのセット)がある場合、どちらか一方のみが必要な場所で両方を交換して使用できます。

これを説明するために、アプリケーションにWarehouseという別のクラスがあると想像してください。

class Warehouse {
  constructor(
    public identifier: string
  ) {}
}

Employeeと同じ形です。 そのインスタンスをprintEmployeeIdentifierに渡そうとした場合:

class Employee {
  constructor(
    public identifier: string
  ) {}
}

class Warehouse {
  constructor(
    public identifier: string
  ) {}
}

function printEmployeeIdentifier(employee: Employee) {
  console.log(employee.identifier);
}

const warehouse = new Warehouse("abc");

printEmployeeIdentifier(warehouse);

TypeScriptコンパイラは文句を言いません。 クラスのインスタンスの代わりに、通常のオブジェクトだけを使用することもできます。 これにより、TypeScriptを使い始めたばかりのプログラマーが予期しない動作が発生する可能性があるため、これらのシナリオに注意を払うことが重要です。

クラスをタイプとして使用する基本が邪魔にならないので、形状だけでなく、特定のクラスをチェックする方法を学ぶことができます。

thisのタイプ

クラス自体のいくつかのメソッド内で現在のクラスのタイプを参照する必要がある場合があります。 このセクションでは、thisを使用してこれを実現する方法を説明します。

EmployeeクラスにisSameEmployeeAsという新しいメソッドを追加する必要があるとします。このメソッドは、別の従業員インスタンスが現在の従業員と同じ従業員を参照しているかどうかを確認します。 これを行う1つの方法は、次のようになります。

class Employee {
  constructor(
    protected identifier: string
  ) {}

  getIdentifier() {
    return this.identifier;
  }

  isSameEmployeeAs(employee: Employee) {
    return this.identifier === employee.identifier;
  }
}

このテストは、Employeeから派生したすべてのクラスのidentifierプロパティを比較するために機能します。 ただし、Employeeの特定のサブクラスをまったく比較したくないシナリオを想像してみてください。 この場合、比較のブール値を受け取る代わりに、2つの異なるサブクラスが比較されたときにTypeScriptがエラーを報告するようにします。

たとえば、財務部門とマーケティング部門の従業員用に2つの新しいサブクラスを作成します。

...
class FinanceEmployee extends Employee {
  specialFieldToFinanceEmployee = '';
}

class MarketingEmployee extends Employee {
  specialFieldToMarketingEmployee = '';
}

const finance = new FinanceEmployee("fin-123");
const marketing = new MarketingEmployee("mkt-123");

marketing.isSameEmployeeAs(finance);

ここでは、Employee基本クラスからFinanceEmployeeMarketingEmployeeの2つのクラスを派生させます。 それぞれに異なる新しいフィールドがあります。 次に、それぞれのインスタンスを1つ作成し、marketingの従業員がfinanceの従業員と同じであるかどうかを確認します。 このシナリオでは、サブクラスはまったく比較されないため、TypeScriptはエラーを報告する必要があります。 isSameEmployeeAsメソッドのemployeeパラメーターの型としてEmployeeを使用したため、これは発生しません。Employeeから派生したすべてのクラスが型を渡します。 -チェック中。

このコードを改善するために、クラス内で使用可能な特別なタイプであるthisタイプを使用できます。 このタイプは、現在のクラスのタイプに動的に設定されます。 このように、このメソッドが派生クラスで呼び出されると、thisが派生クラスのタイプに設定されます。

代わりにthisを使用するようにコードを変更してください。

class Employee {
  constructor(
    protected identifier: string
  ) {}

  getIdentifier() {
    return this.identifier;
  }

  isSameEmployeeAs(employee: this) {
    return this.identifier === employee.identifier;
  }
}

class FinanceEmployee extends Employee {
  specialFieldToFinanceEmployee = '';
}

class MarketingEmployee extends Employee {
  specialFieldToMarketingEmployee = '';
}

const finance = new FinanceEmployee("fin-123");
const marketing = new MarketingEmployee("mkt-123");

marketing.isSameEmployeeAs(finance);

このコードをコンパイルすると、TypeScriptコンパイラはエラー2345を表示するようになりました。

Output
Argument of type 'FinanceEmployee' is not assignable to parameter of type 'MarketingEmployee'. Property 'specialFieldToMarketingEmployee' is missing in type 'FinanceEmployee' but required in type 'MarketingEmployee'. (2345)

thisキーワードを使用すると、さまざまなクラスコンテキストで入力を動的に変更できます。 次に、クラスのインスタンスではなく、クラス自体を渡すために入力を使用します。

コンストラクト署名の使用

プログラマーは、インスタンスではなく、クラスを直接受け取る関数を作成する必要がある場合があります。 そのためには、構成シグネチャを持つ特別なタイプを使用する必要があります。 このセクションでは、そのようなタイプを作成する方法について説明します。

クラス自体を渡す必要がある特定のシナリオの1つは、クラスファクトリ、または引数として渡されるクラスの新しいインスタンスを生成する関数です。 Employeeに基づくクラスを取得し、増分された識別子を使用して新しいインスタンスを作成し、その識別子をコンソールに出力する関数を作成するとします。 次のようにこれを作成しようとするかもしれません:

class Employee {
  constructor(
    public identifier: string
  ) {}
}

let identifier = 0;
function createEmployee(ctor: Employee) {
  const employee = new ctor(`test-${identifier++}`);
  console.log(employee.identifier);
}

このスニペットでは、Employeeクラスを作成し、identifierを初期化し、Employee。 しかし、このコードをコンパイルしようとすると、TypeScriptコンパイラはエラー2351を出します。

Output
This expression is not constructable. Type 'Employee' has no construct signatures. (2351)

これは、クラスの名前をctorの型として使用する場合、その型はクラスのインスタンスに対してのみ有効であるために発生します。 クラスコンストラクター自体の型を取得するには、typeof ClassNameを使用する必要があります。 次の強調表示されたコードを変更して確認してください。

class Employee {
  constructor(
    public identifier: string
  ) {}
}

let identifier = 0;
function createEmployee(ctor: typeof Employee) {
  const employee = new ctor(`test-${identifier++}`);
  console.log(employee.identifier);
}

これで、コードは正常にコンパイルされます。 ただし、まだ保留中の問題があります。クラスファクトリは基本クラスから構築された新しいクラスのインスタンスを構築するため、abstractクラスを使用するとワークフローが改善される可能性があります。 ただし、これは最初は機能しません。

これを試すには、Employeeクラスをabstractクラスに変換します。

abstract class Employee {
  constructor(
    public identifier: string
  ) {}
}

let identifier = 0;
function createEmployee(ctor: typeof Employee) {
  const employee = new ctor(`test-${identifier++}`);
  console.log(employee.identifier);
}

TypeScriptコンパイラはエラー2511を出すようになりました:

Output
Cannot create an instance of an abstract class. (2511)

このエラーは、abstractであるため、Employeeクラスからインスタンスを作成できないことを示しています。 ただし、このような関数を使用して、Employee抽象クラスから拡張するさまざまな種類の従業員を次のように作成することもできます。

abstract class Employee {
  constructor(
    public identifier: string
  ) {}
}

class FinanceEmployee extends Employee {}

class MarketingEmployee extends Employee {}

let identifier = 0;
function createEmployee(ctor: typeof Employee) {
  const employee = new ctor(`test-${identifier++}`);
  console.log(employee.identifier);
}

createEmployee(FinanceEmployee);
createEmployee(MarketingEmployee);

このシナリオでコードを機能させるには、コンストラクターシグネチャを持つ型を使用する必要があります。 これを行うには、newキーワードを使用し、その後に矢印関数と同様の構文を使用します。ここで、パラメーターリストにはコンストラクターが期待するパラメーターが含まれ、戻り型はこのコンストラクターが返すクラスインスタンスです。

次のコードで強調表示されているのは、コンストラクター署名付きの型をcreateEmployee関数に導入する変更です。

abstract class Employee {
  constructor(
    public identifier: string
  ) {}
}

class FinanceEmployee extends Employee {}

class MarketingEmployee extends Employee {}

let identifier = 0;
function createEmployee(ctor: new (identifier: string) => Employee) {
  const employee = new ctor(`test-${identifier++}`);
  console.log(employee.identifier);
}

createEmployee(FinanceEmployee);
createEmployee(MarketingEmployee);

TypeScriptコンパイラがコードを正しくコンパイルするようになりました。

結論

TypeScriptのクラスは、型システム、矢印関数メソッドなどの追加の構文、およびメンバーの可視性や抽象クラスなどの完全に新しい機能にアクセスできるため、JavaScriptよりもさらに強力です。 これにより、タイプセーフで信頼性が高く、アプリケーションのビジネスモデルをより適切に表すコードを提供できます。

TypeScriptのその他のチュートリアルについては、TypeScriptシリーズのコーディング方法ページをご覧ください。