クラスとは、データとそれを操作する機能をひとまとめにした設計図のようなもの。特にTypeScriptのようなオブジェクト指向言語では、クラスはアプリケーションの基本的な構成要素として機能し、この設計図をもとに作成される実体(=オブジェクト)を通じてさまざまなデータや処理を管理し、複雑な機能を簡単に実装できるようになります。
クラスを使用することでより安全で保守しやすいコードを書くことが可能になります。本記事では、TypeScriptにおけるクラスの基本的な定義方法から、より高度な利用方法まで、ステップバイステップで解説していきます。初心者の方でも理解しやすいように、基礎からしっかりと学べる内容となっていますので、是非最後までご一読ください。
TypeScript:クラスとは
クラスとは、オブジェクトの青写真(=設計書)です。具体的にいうと、クラスはデータ(属性)とそのデータに操作を加えるメソッド(関数)を1つのまとまりとして定義したものを指します。TypeScriptでは、このクラスを使用してデータ構造を定義し、その構造に沿ったオブジェクトを生成することができます。
クラスの基本的な構造には、以下のような要素が含まれます。
- プロパティ(属性)
- クラスに属するデータの変数。オブジェクトの状態を表します。
- メソッド
- クラスに属する関数。オブジェクトの振る舞いを定義。プロパティの値を操作したり、何らかの処理を行うために使用されます。
- コンストラクタ
- クラスからオブジェクトを生成する際に自動的に呼び出される特殊なメソッド。オブジェクトの初期化に用いられます。
TypeScriptでは、これらのクラス要素を利用することで、コードの再利用性を高め、複雑なアプリケーションでも管理しやすい構造を作ることを可能にします。
例えば、車を表すクラスを作成する場合、プロパティとして「色」「車種」を、メソッドとして「加速する」「停止する」を定義。このように定義することで、同じ構造を持つ複数の車オブジェクトを簡単に作成し、それぞれに異なる属性を持たせることができます。
class Car { // プロパティの定義 color: string; model: string; // コンストラクタの定義 constructor(color: string, model: string) { this.color = color; this.model = model; } // メソッドの定義 accelerate() { console.log(`${this.model} is accelerating.`); } stop() { console.log(`${this.model} has stopped.`); } } // Carクラスのオブジェクトを生成 const myCar = new Car('red', 'Toyota'); myCar.accelerate(); // 出力: Toyota is accelerating. myCar.stop(); // 出力: Toyota has stopped.
クラスの定義方法はJavascriptとほとんど同じ。「型」を指定する必要があるという点を除けば、その概念も役割もほとんど同じです。
クラスの定義方法
class Person { // プロパティの定義 name: string; age: number; // コンストラクタの定義 constructor(name: string, age: number) { this.name = name; this.age = age; } // メソッドの定義 describe() { console.log(`${this.name} is ${this.age} years old.`); } } // Personクラスのインスタンスを作成 const person1 = new Person('Alice', 30); person1.describe(); // 出力: Alice is 30 years old.
- クラスキーワードの使用
class
キーワードを利用
- クラス名の指定
- クラスを識別するための名前が必要。名前は大文字で始めることが一般的。
- プロパティの定義
- クラス内で使用されるデータ(プロパティ)を定義。これらはクラスのインスタンス毎に異なる値を持つことができます。
- コンストラクタの定義
- 必要に応じて、
constructor
メソッドを定義して、クラスのインスタンスが作成された際の初期化処理を記述。
- 必要に応じて、
- メソッドの追加
- クラスが行うべき操作をメソッドとして追加。プロパティの値を変更するメソッドや、何らかの処理を実行するメソッドが含まれます。
↑の例では、Person
という名前のクラスを定義し、name
とage
という2つのプロパティ、およびdescribe
というメソッドを持っています。new Person('Alice', 30)
を使ってPerson
クラスのインスタンスを生成し、そのインスタンスに対してdescribe
メソッドを呼び出しています。
プロパティ
プロパティは、そのクラスのインスタンスに関連付けられたデータ(=変数)です。プロパティは通常クラスの先頭部分で定義されます。TypeScriptでは、プロパティの型を明示的に宣言することで、型安全性とコードの読みやすさを向上させます。
class Vehicle { color: string; constructor(color: string) { this.color = color; // プロパティの初期化 } }
メソッド
メソッドはクラスに属する関数であり、クラスのインスタンスが行う操作を定義します。メソッド内でthis
キーワードを使用することで、同じクラスの他のメソッドやプロパティにアクセスできます。
class Vehicle { color: string; constructor(color: string) { this.color = color; } describe() { console.log(`This vehicle is ${this.color}.`); } }
アクセス修飾子
TypeScriptでは、プロパテやメソッドに対してアクセス修飾子を使用することができます。
アクセス修飾子とは、簡単に言えばそのプロパティ/メソッドが、プログラムの他の部分から参照できるのか?参照できないのか?を表すもの。
アクセス修飾子にはpublic
、private
、protected
の3種類があり、それぞれがメンバーの可視性とアクセス範囲を定義します。
- public: どこからでもアクセス可能。デフォルトは
public
として扱われる。 - private: メンバーが定義されたクラス内からのみアクセス可能。クラスの外部からはアクセスできない。
- protected:
private
と似ていますが、派生クラスからのアクセスも許可される。
class Vehicle { private color: string; constructor(color: string) { this.color = color; } public describe() { console.log(`This vehicle is ${this.color}.`); } }
上記の「color」はprivate
であるため、外部から「Vehicle.color」のような形で参照することができません。クラス内でのみ利用可能だよ!ということを示しています。
アクセス修飾子を使用することで、クラスの使用方法を制限し、予期しない方法での利用を防止することが可能になります。また、クラスの設計者が意図した安全なインターフェースを提供することができます。
ゲッターとセッター
TypeScriptでは、プロパティの値へのアクセスを制御するために、ゲッター(getter)とセッター(setter)を定義することができます。これらはそれぞれ、プロパティの値を取得または設定するメソッドとして機能します。ゲッターとセッターを使用することで、プロパティへのアクセスをより詳細に制御し、外部からの不適切な値の設定を防ぐことが可能になります。
class Vehicle { private _color: string; constructor(color: string) { this._color = color; } get color() { return this._color; } set color(value: string) { if(value === '') throw new Error("Color cannot be empty."); this._color = value; } } const vehicle = new Vehicle("red"); console.log(vehicle.color); // "red" vehicle.color = "blue"; // セッターを通じて値を設定 console.log(vehicle.color); // "blue"
本来、プロパティ_color
はprivate
で宣言されており、クラスの外部から直接アクセスすることはできません。ですが代わりに、ゲッターとセッターを通じて、間接的にこのプロパティの値を取得または設定しています。このテクニックを用いることで、値の検証や処理をゲッター/セッター内で実施することができ、より安全なプログラミングが可能になります。
継承
継承は、オブジェクト指向プログラミングの中心的な概念の1つ。クラス間でコードを再利用するためのメカニズムです。継承を使用することで、既存のクラス(親クラス、またはスーパークラスとも呼ばれる)のプロパティやメソッドを新しいクラス(子クラス、またはサブクラスとも呼ばれる)が受け継ぎ、それらを使用、拡張、または変更することができます。このプロセスにより、コードの重複を減らし、プログラムの構造をより管理しやすく、拡張しやすいものにすることができます。
TypeScriptでは、extends
キーワードを使用して継承を実装します。サブクラスはスーパークラスのすべての公開(public)メンバーと保護(protected)メンバーを継承しますが、プライベート(private)メンバーは継承されません。サブクラスは継承したメンバーに加えて、独自のメンバーを持つことができます。
例として、Animal
クラスを親クラスとし、このクラスからDog
クラスとCat
クラスを派生させることを考えてみましょう。
Animal
クラスには、すべての動物に共通のプロパティやメソッドが定義されているとします。Dog
クラスとCat
クラスでは、これらを継承しつつ、種に特有の振る舞いを追加することができます。
class Animal { name: string; constructor(name: string) { this.name = name; } makeSound() { console.log('Some generic animal sound'); } } class Dog extends Animal { constructor(name: string) { super(name); // 親クラスのコンストラクタを呼び出す } makeSound() { console.log('Woof'); } } class Cat extends Animal { constructor(name: string) { super(name); } makeSound() { console.log('Meow'); } }
この例では、Dog
とCat
の両クラスがAnimal
クラスからname
プロパティとmakeSound
メソッドを継承していますが、それぞれのmakeSound
メソッドをオーバーライドして独自の音を出すようにしています。これにより、同じメソッドを呼び出しても、オブジェクトの型に応じて異なる結果が得られるようになります。これが継承の実例です。
ポイント 継承の主な利点
- コードの再利用
- 既存のクラスの機能を基にして新しいクラスを作成することで、コードを効率的に再利用することができる。
- 拡張性
- 既存のクラスに新しい機能を追加したり、既存の機能をカスタマイズすることが簡単になる。
- 階層的なクラス構造
- クラス間の関係を階層的に構築することができ、より組織的で理解しやすいコードベースを作成することが可能になる。この階層構造により、共通の機能は上位のクラスに、特有の機能は下位のクラスに配置することで、クラスの専門化を進めることができる。
- 多様性の実現
- サブクラスはスーパークラスのメソッドをオーバーライド(上書き)することができ、同一のインターフェースを持ちながら異なる振る舞いを実装することが可能になります。これにより、同じ種類のオブジェクトでも異なる動作をさせることができるため、柔軟な設計が実現されます。
ポリモーフィズム(多態性)
ポリモーフィズムは、オブジェクト指向プログラミングにおける重要な概念の1つ。「多態性」または「多形性」とも呼ばれます。
ポリモーフィズムを利用することで、異なるクラスのオブジェクトが同じインターフェースやメソッドを通じて操作されることを可能にし、コードの柔軟性と再利用性を高めることができます。ポリモーフィズムには主に2つの形式があります:サブタイプポリモーフィズム(継承によるポリモーフィズム)とアドホックポリモーフィズム(オーバーロードやジェネリクスによるポリモーフィズム)
1. サブタイプポリモーフィズム(継承によるポリモーフィズム)
サブタイプポリモーフィズムは、クラスの継承を利用して実現されます。基底クラス(またはインターフェース)の参照を通じて、派生クラスのオブジェクトを操作することができます。これにより、異なるサブクラスのオブジェクトを同一の方法で扱うことが可能になります。
// スーパークラス:Animal class Animal { name: string; constructor(name: string) { this.name = name; } // 共通の行動をメソッドで定義 makeSound() { console.log("Some generic sound"); } } // サブクラス:Dog class Dog extends Animal { // Dogクラス固有のメソッド wagTail() { console.log("Wagging tail"); } // スーパークラスのメソッドをオーバーライド(上書き) makeSound() { console.log("Woof!"); } } // サブクラス:Cat class Cat extends Animal { // Catクラス固有のメソッド chaseMouse() { console.log("Chasing a mouse"); } // スーパークラスのメソッドをオーバーライド(上書き) makeSound() { console.log("Meow"); } } // Animal型の配列に、DogとCatのインスタンスを追加 const animals: Animal[] = [new Dog("Rex"), new Cat("Whiskers")]; // 各動物のmakeSoundメソッドを呼び出す animals.forEach(animal => animal.makeSound());
この例では、Animal
クラスが基底クラス(スーパークラス)として定義され、Dog
とCat
がそれを継承しています(サブクラス)。Dog
とCat
は、Animal
のmakeSound
メソッドをそれぞれの鳴き声に合わせてオーバーライドしています。さらに、それぞれのクラスには、固有の行動を表すメソッド(wagTail
、chaseMouse
)が追加されています。
このコードのポイントは、異なる種類のオブジェクト(Dog
、Cat
)を、共通のスーパークラス型(Animal
)の配列に格納し、共通のインターフェース(makeSound
メソッド)を介して、それぞれ異なる具体的な振る舞いを実行させることができる点です。これにより、コードの柔軟性と再利用性が高まります。
2. アドホックポリモーフィズム
アドホックポリモーフィズムは、関数のオーバーロードやジェネリクスを通じて実現されます。これにより、同じ関数名でも異なる型の引数に対応したり、同一のインターフェースを持つが内部的に異なる挙動をする関数を定義することができます。
関数のオーバーロード(関数オーバーロード)
TypeScriptでは、同じ関数名でも異なる引数の型や数に応じて異なる実装を提供することができます。ただし、TypeScriptでは直接的なオーバーロードの実装はサポートされていないため、シグネチャをオーバーロードする形で実現します。
function greet(name: string): string; function greet(age: number): string; function greet(value: string | number): string { if (typeof value === 'string') { return `Hello, ${value}`; } else { return `You are ${value} years old`; } } console.log(greet("Alice")); // Hello, Alice console.log(greet(25)); // You are 25 years old
ジェネリクス(ジェネリックプログラミング)
ジェネリクスを使用すると、型をパラメータとして関数やクラスに渡すことができ、コードの再利用性を高めることが可能になります。
function identity<T>(arg: T): T { return arg; } let output1 = identity<string>("myString"); let output2 = identity<number>(100); console.log(output1); // myString console.log(output2); // 100
identity
関数はどのような型の引数も受け取り、同じ型の値を返します。ジェネリクスを用いることで、関数の呼び出し時に型を指定し、様々な型に対して柔軟に対応することができます。