従来のJavaScriptは、関数とプロトタイプベースの継承を使用して再利用可能なコンポーネントを構築しますが、これはオブジェクト指向のアプローチに慣れているプログラマにとっては少しぎこちなく感じるかもしれません。オブジェクト指向のアプローチでは、クラスが機能を継承し、オブジェクトはこれらのクラスから構築されます。ECMAScript 2015(ECMAScript 6とも呼ばれる)以降、JavaScriptプログラマは、このオブジェクト指向のクラスベースのアプローチを使用してアプリケーションを構築できるようになりました。TypeScriptでは、開発者がこれらのテクニックをすぐに使用し、主要なすべてのブラウザとプラットフォームで動作するJavaScriptにコンパイルすることを可能にします。次のバージョンのJavaScriptを待つ必要はありません。
クラス
簡単なクラスベースの例を見てみましょう。
tsTry
classGreeter {greeting : string;constructor(message : string) {this.greeting =message ;}greet () {return "Hello, " + this.greeting ;}}letgreeter = newGreeter ("world");
以前C#またはJavaを使用していたことがある場合は、構文がなじみがあるはずです。新しいクラス`Greeter`を宣言します。このクラスには、`greeting`というプロパティ、コンストラクタ、`greet`メソッドの3つのメンバーがあります。
クラス内でクラスのメンバーを参照する際には、`this.`を前に付けることに注意してください。これはメンバーアクセスであることを示しています。
最後の行では、`new`を使用して`Greeter`クラスのインスタンスを構築します。これにより、以前に定義したコンストラクタが呼び出され、`Greeter`の形状を持つ新しいオブジェクトが作成され、コンストラクタが実行されて初期化されます。
継承
TypeScriptでは、一般的なオブジェクト指向パターンを使用できます。クラスベースのプログラミングにおける最も基本的なパターンの1つは、継承を使用して既存のクラスを拡張して新しいクラスを作成できることです。
例を見てみましょう。
tsTry
classAnimal {move (distanceInMeters : number = 0) {console .log (`Animal moved ${distanceInMeters }m.`);}}classDog extendsAnimal {bark () {console .log ("Woof! Woof!");}}constdog = newDog ();dog .bark ();dog .move (10);dog .bark ();
この例は、最も基本的な継承機能を示しています。クラスは、基底クラスからプロパティとメソッドを継承します。ここで、`Dog`は、`extends`キーワードを使用して`Animal`基底クラスから派生する*派生*クラスです。派生クラスはしばしば*サブクラス*と呼ばれ、基底クラスはしばしば*スーパークラス*と呼ばれます。
`Dog`は`Animal`からの機能を継承しているため、`bark()`と`move()`の両方ができる`Dog`のインスタンスを作成できました。
より複雑な例を見てみましょう。
tsTry
classAnimal {name : string;constructor(theName : string) {this.name =theName ;}move (distanceInMeters : number = 0) {console .log (`${this.name } moved ${distanceInMeters }m.`);}}classSnake extendsAnimal {constructor(name : string) {super(name );}move (distanceInMeters = 5) {console .log ("Slithering...");super.move (distanceInMeters );}}classHorse extendsAnimal {constructor(name : string) {super(name );}move (distanceInMeters = 45) {console .log ("Galloping...");super.move (distanceInMeters );}}letsam = newSnake ("Sammy the Python");lettom :Animal = newHorse ("Tommy the Palomino");sam .move ();tom .move (34);
この例では、これまで説明しなかった他のいくつかの機能について説明します。再び、`extends`キーワードを使用して`Animal`の2つの新しいサブクラスである`Horse`と`Snake`を作成します。
以前の例との違いの1つは、コンストラクタ関数を含む派生クラスは必ず`super()`を呼び出す必要があることです。これにより、基底クラスのコンストラクタが実行されます。さらに、コンストラクタの本体で`this`のプロパティにアクセスする前に、必ず`super()`を呼び出す必要があります。これはTypeScriptが強制する重要なルールです。
この例は、基底クラスのメソッドをサブクラス用に特化したメソッドでオーバーライドする方法も示しています。`Snake`と`Horse`の両方が、`Animal`の`move`メソッドをオーバーライドする`move`メソッドを作成し、各クラスに固有の機能を提供しています。`tom`は`Animal`として宣言されていますが、その値は`Horse`であるため、`tom.move(34)`を呼び出すと、`Horse`のオーバーライドメソッドが呼び出されます。
Slithering... Sammy the Python moved 5m. Galloping... Tommy the Palomino moved 34m.
public、private、protected修飾子
デフォルトで public
これまでの例では、プログラム全体で宣言したメンバに自由にアクセスできました。他の言語のクラスに精通している場合、上記の例ではこれを達成するために `public` という単語を使用する必要がなかったことに気づいたかもしれません。たとえば、C#では、各メンバを明示的に `public` とラベル付けしないと可視になりません。TypeScriptでは、各メンバはデフォルトで `public` です。
メンバを明示的に `public` とマークすることもできます。前のセクションの `Animal` クラスを次のように記述することもできました。
tsTry
classAnimal {publicname : string;public constructor(theName : string) {this.name =theName ;}publicmove (distanceInMeters : number) {console .log (`${this.name } moved ${distanceInMeters }m.`);}}
ECMAScript プライベートフィールド
TypeScript 3.8 では、プライベートフィールドに関する新しい JavaScript 構文がサポートされています。
tsTry
classAnimal {#name: string;constructor(theName : string) {this.#name =theName ;}}newProperty '#name' is not accessible outside class 'Animal' because it has a private identifier.18013Property '#name' is not accessible outside class 'Animal' because it has a private identifier.Animal ("Cat").#name ;
この構文は JavaScript ランタイムに組み込まれており、各プライベートフィールドの分離についてより確実な保証を行うことができます。現時点では、これらのプライベートフィールドに関する最良のドキュメントは、TypeScript 3.8 の リリースノート にあります。
TypeScript の `private` の理解
TypeScript は、メンバを `private` としてマークする方法も提供しており、包含クラスの外からはアクセスできません。例:
tsTry
classAnimal {privatename : string;constructor(theName : string) {this.name =theName ;}}newProperty 'name' is private and only accessible within class 'Animal'.2341Property 'name' is private and only accessible within class 'Animal'.Animal ("Cat").; name
TypeScript は構造的型システムです。2 つの異なる型を比較する場合、それらがどこから来たかに関係なく、すべてのメンバの型が互換性がある場合、型自体が互換性があると見なします。
ただし、`private` および `protected` メンバを持つ型を比較する場合、これらの型は異なる方法で扱われます。2 つの型が互換性があると見なされるためには、一方に `private` メンバがある場合、他方にも同じ宣言で生成された `private` メンバが必要です。`protected` メンバについても同様です。
これが実際にはどのように機能するかをよりよく理解するために、例を見てみましょう。
tsTry
classAnimal {privatename : string;constructor(theName : string) {this.name =theName ;}}classRhino extendsAnimal {constructor() {super("Rhino");}}classEmployee {privatename : string;constructor(theName : string) {this.name =theName ;}}letanimal = newAnimal ("Goat");letrhino = newRhino ();letemployee = newEmployee ("Bob");animal =rhino ;Type 'Employee' is not assignable to type 'Animal'. Types have separate declarations of a private property 'name'.2322Type 'Employee' is not assignable to type 'Animal'. Types have separate declarations of a private property 'name'.= animal employee ;
この例では、`Animal` と `Rhino` があり、`Rhino` は `Animal` のサブクラスです。また、形状が `Animal` と同じに見える新しいクラス `Employee` もあります。これらのクラスのいくつかのインスタンスを作成し、互いに代入しようと試みて、何が起こるかを確認します。`Animal` と `Rhino` は、`Animal` の `private name: string` の同じ宣言から `private` の側面を共有しているため、互換性があります。しかし、`Employee` はそうではありません。`Employee` から `Animal` に代入しようとすると、これらの型は互換性がないというエラーが発生します。`Employee` にも `name` という `private` メンバがありますが、それは `Animal` で宣言したものとは異なります。
protected
の理解
protected
修飾子は、`private` 修飾子と非常によく似ていますが、`protected` と宣言されたメンバは、派生クラス内からもアクセスできる点が異なります。例:
tsTry
classPerson {protectedname : string;constructor(name : string) {this.name =name ;}}classEmployee extendsPerson {privatedepartment : string;constructor(name : string,department : string) {super(name );this.department =department ;}publicgetElevatorPitch () {return `Hello, my name is ${this.name } and I work in ${this.department }.`;}}lethoward = newEmployee ("Howard", "Sales");console .log (howard .getElevatorPitch ());Property 'name' is protected and only accessible within class 'Person' and its subclasses.2445Property 'name' is protected and only accessible within class 'Person' and its subclasses.console .log (howard .); name
`Person` の外部では `name` を使用できませんが、`Employee` は `Person` から派生しているため、`Employee` のインスタンスメソッド内では引き続き使用できます。
コンストラクタも `protected` とマークできます。これは、クラスを包含クラスの外でインスタンス化することはできませんが、拡張できることを意味します。例:
tsTry
classPerson {protectedname : string;protected constructor(theName : string) {this.name =theName ;}}// Employee can extend PersonclassEmployee extendsPerson {privatedepartment : string;constructor(name : string,department : string) {super(name );this.department =department ;}publicgetElevatorPitch () {return `Hello, my name is ${this.name } and I work in ${this.department }.`;}}lethoward = newEmployee ("Howard", "Sales");letConstructor of class 'Person' is protected and only accessible within the class declaration.2674Constructor of class 'Person' is protected and only accessible within the class declaration.john = newPerson ("John");
Readonly 修飾子
readonly
キーワードを使用することで、プロパティを readonly にすることができます。Readonly プロパティは、宣言時またはコンストラクタで初期化する必要があります。
tsTry
classOctopus {readonlyname : string;readonlynumberOfLegs : number = 8;constructor(theName : string) {this.name =theName ;}}letdad = newOctopus ("Man with the 8 strong legs");Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.dad .= "Man with the 3-piece suit"; name
パラメータプロパティ
最後の例では、`Octopus` クラスで `readonly` メンバ `name` とコンストラクタパラメータ `theName` を宣言する必要がありました。これは、`Octopus` コンストラクタの実行後に `theName` の値にアクセスできるようにするために必要です。*パラメータプロパティ*を使用すると、メンバを1箇所で作成および初期化できます。パラメータプロパティを使用した前の `Octopus` クラスのさらに改訂版を次に示します。
tsTry
classOctopus {readonlynumberOfLegs : number = 8;constructor(readonlyname : string) {}}letdad = newOctopus ("Man with the 8 strong legs");dad .name ;
`theName` を完全に削除し、コンストラクタで短縮された `readonly name: string` パラメータを使用して `name` メンバを作成および初期化する方法に注目してください。宣言と代入を1箇所に統合しました。
パラメータプロパティは、コンストラクタパラメータの前にアクセス修飾子または `readonly`、またはその両方を付けることで宣言されます。パラメータプロパティに `private` を使用すると、プライベートメンバが宣言および初期化されます。同様に、`public`、`protected`、`readonly` についても同じことが行われます。
アクセサ
TypeScript は、ゲッター/セッターをオブジェクトのメンバへのアクセスをインターセプトする方法としてサポートしています。これにより、各オブジェクトのメンバへのアクセス方法をより細かく制御できます。
`get` と `set` を使用するように単純なクラスを変換してみましょう。まず、ゲッターとセッターを使用しない例から始めましょう。
tsTry
classEmployee {fullName : string;}letemployee = newEmployee ();employee .fullName = "Bob Smith";if (employee .fullName ) {console .log (employee .fullName );}
人々が `fullName` を直接設定できるようにすることは非常に便利ですが、`fullName` が設定されるときにいくつかの制約を適用したい場合もあります。
このバージョンでは、`newName` の長さを確認して、バックエンドデータベースフィールドの最大長と互換性があることを確認するセッターを追加します。互換性がない場合は、エラーをスローして、クライアントコードに何か問題が発生したことを通知します。
既存の機能を維持するために、`fullName` を変更せずに取得する単純なゲッターも追加します。
tsTry
constfullNameMaxLength = 10;classEmployee {private_fullName : string = "";getfullName (): string {return this._fullName ;}setfullName (newName : string) {if (newName &&newName .length >fullNameMaxLength ) {throw newError ("fullName has a max length of " +fullNameMaxLength );}this._fullName =newName ;}}letemployee = newEmployee ();employee .fullName = "Bob Smith";if (employee .fullName ) {console .log (employee .fullName );}
アクセサが値の長さをチェックしていることを確認するために、10文字を超える名前を代入しようと試みて、エラーが発生することを確認できます。
アクセサに関する注意点がいくつかあります。
まず、アクセサを使用するには、コンパイラを ECMAScript 5 以降を出力するように設定する必要があります。ECMAScript 3 へのダウングレードはサポートされていません。第二に、`get` はあり `set` がないアクセサは、自動的に `readonly` と推論されます。これは、コードから `.d.ts` ファイルを生成する場合に役立ちます。なぜなら、プロパティのユーザーは、それを変更できないことがわかるからです。
静的プロパティ
これまでは、クラスの*インスタンス*メンバ、つまりインスタンス化されたときにオブジェクトに表示されるものについてのみ説明してきました。クラスの*静的*メンバ、つまりインスタンスではなくクラス自体に表示されるメンバを作成することもできます。この例では、`static` を起点で使用しています。これは、すべてのグリッドの一般的な値であるためです。各インスタンスは、クラスの名前を前に付けることでこの値にアクセスします。インスタンスへのアクセス前に `this.` を付けるのと同様に、ここでは静的アクセス前に `Grid.` を付けます。
tsTry
classGrid {staticorigin = {x : 0,y : 0 };calculateDistanceFromOrigin (point : {x : number;y : number }) {letxDist =point .x -Grid .origin .x ;letyDist =point .y -Grid .origin .y ;returnMath .sqrt (xDist *xDist +yDist *yDist ) / this.scale ;}constructor(publicscale : number) {}}letgrid1 = newGrid (1.0); // 1x scaleletgrid2 = newGrid (5.0); // 5x scaleconsole .log (grid1 .calculateDistanceFromOrigin ({x : 10,y : 10 }));console .log (grid2 .calculateDistanceFromOrigin ({x : 10,y : 10 }));
抽象クラス
抽象クラスは、他のクラスを派生させるための基底クラスです。直接インスタンス化することはできません。インターフェースとは異なり、抽象クラスにはメンバーの実装の詳細を含めることができます。`abstract`キーワードは、抽象クラスと、抽象クラス内の抽象メソッドを定義するために使用されます。
tsTry
abstract classAnimal {abstractmakeSound (): void;move (): void {console .log ("roaming the earth...");}}
抽象としてマークされた抽象クラス内のメソッドには実装が含まれておらず、派生クラスで実装する必要があります。抽象メソッドは、インターフェースメソッドと同様の構文を共有します。どちらもメソッド本体を含めずにメソッドのシグネチャを定義します。ただし、抽象メソッドには`abstract`キーワードを含める必要があり、アクセス修飾子を含めることもできます。
tsTry
abstract classDepartment {constructor(publicname : string) {}printName (): void {console .log ("Department name: " + this.name );}abstractprintMeeting (): void; // must be implemented in derived classes}classAccountingDepartment extendsDepartment {constructor() {super("Accounting and Auditing"); // constructors in derived classes must call super()}printMeeting (): void {console .log ("The Accounting Department meets each Monday at 10am.");}generateReports (): void {console .log ("Generating accounting reports...");}}letdepartment :Department ; // ok to create a reference to an abstract typeCannot create an instance of an abstract class.2511Cannot create an instance of an abstract class.department = newDepartment (); // error: cannot create an instance of an abstract classdepartment = newAccountingDepartment (); // ok to create and assign a non-abstract subclassdepartment .printName ();department .printMeeting ();Property 'generateReports' does not exist on type 'Department'.2339Property 'generateReports' does not exist on type 'Department'.department .(); // error: department is not of type AccountingDepartment, cannot access generateReports generateReports
高度なテクニック
コンストラクタ関数
TypeScriptでクラスを宣言すると、実際には同時に複数の宣言を作成しています。最初のものは、クラスのインスタンスの型です。
tsTry
classGreeter {greeting : string;constructor(message : string) {this.greeting =message ;}greet () {return "Hello, " + this.greeting ;}}letgreeter :Greeter ;greeter = newGreeter ("world");console .log (greeter .greet ()); // "Hello, world"
ここで、`let greeter: Greeter`と述べるとき、`Greeter`をクラス`Greeter`のインスタンスの型として使用しています。これは、他のオブジェクト指向言語からのプログラマーにとってほぼ当たり前のことです。
また、コンストラクタ関数と呼ばれる別の値も作成しています。これは、クラスのインスタンスを`new`で作成する際に呼び出される関数です。これが実際にはどのようなものかを見るために、上記の例によって作成されたJavaScriptを見てみましょう。
tsTry
letGreeter = (function () {functionGreeter (message ) {this.greeting =message ;}Greeter .prototype .greet = function () {return "Hello, " + this.greeting ;};returnGreeter ;})();letgreeter ;greeter = newGreeter ("world");console .log (greeter .greet ()); // "Hello, world"
ここで、`let Greeter`にはコンストラクタ関数が割り当てられます。`new`を呼び出してこの関数を実行すると、クラスのインスタンスが取得されます。コンストラクタ関数には、クラスのすべての静的メンバーも含まれています。各クラスを別の方法で考えるもう一つの方法は、インスタンス側と静的側の2つがあるということです。
この違いを示すために、例を少し変更してみましょう。
tsTry
classGreeter {staticstandardGreeting = "Hello, there";greeting : string;greet () {if (this.greeting ) {return "Hello, " + this.greeting ;} else {returnGreeter .standardGreeting ;}}}letgreeter1 :Greeter ;greeter1 = newGreeter ();console .log (greeter1 .greet ()); // "Hello, there"letgreeterMaker : typeofGreeter =Greeter ;greeterMaker .standardGreeting = "Hey there!";letgreeter2 :Greeter = newgreeterMaker ();console .log (greeter2 .greet ()); // "Hey there!"letgreeter3 :Greeter ;greeter3 = newGreeter ();console .log (greeter3 .greet ()); // "Hey there!"
この例では、`greeter1`は以前と同様に機能します。`Greeter`クラスをインスタンス化し、このオブジェクトを使用します。これはこれまで見てきたものです。
次に、クラスを直接使用します。ここでは、`greeterMaker`という新しい変数を作成します。この変数にはクラス自体、つまりコンストラクタ関数が格納されます。ここでは`typeof Greeter`を使用します。つまり、「`Greeter`クラス自体の型をください」という意味であり、インスタンス型ではありません。「`Greeter`と呼ばれるシンボルの型をください」という意味、より正確にはコンストラクタ関数の型です。この型には、`Greeter`クラスのインスタンスを作成するコンストラクタとともに、`Greeter`のすべての静的メンバーが含まれます。これは、`greeterMaker`に対して`new`を使用し、`Greeter`の新しいインスタンスを作成して、以前のように呼び出すことで示しています。静的プロパティの変更は好ましくないことも言及しておきましょう。ここでは、`greeter3`の`standardGreeting`が`「Hello, there」`ではなく`「Hey there!」`になっています。
クラスをインターフェースとして使用
前のセクションで述べたように、クラス宣言は2つのものを作成します。クラスのインスタンスを表す型と、コンストラクタ関数です。クラスは型を作成するため、インターフェースを使用できる場所と同じ場所でクラスを使用できます。
tsTry
classPoint {x : number;y : number;}interfacePoint3d extendsPoint {z : number;}letpoint3d :Point3d = {x : 1,y : 2,z : 3 };