なぜクラスを使うのか
プログラムが大きくなってくると、関連するデータと処理をまとめて管理したくなります。たとえば、ユーザー情報を扱うプログラムを考えてみてください。
// クラスを使わない場合の問題
let userName = "田中";
let userAge = 25;
let userEmail = "[email protected]";
function greetUser() {
return `こんにちは、${userName}です。`;
}
function updateUserAge(newAge: number) {
userAge = newAge;
}
この書き方では、ユーザーが複数いる場合に変数名が衝突し、どの関数がどの変数に対応するのかがわからなくなります。クラスを使うことで、これらの問題を解決できます。
クラスの基本構文
クラスは、データ(プロパティ)と処理(メソッド)をひとまとめにした設計図のようなものです。
class Person {
// プロパティ:このクラスが持つデータ
name: string;
age: number;
// コンストラクタ:インスタンス作成時に呼び出される
constructor(name: string, age: number) {
this.name = name; // 受け取った引数をプロパティに設定
this.age = age;
}
// メソッド:このクラスができる処理
greet(): string {
return `こんにちは、${this.name}です。`;
}
}
// クラスからインスタンス(実際のオブジェクト)を作成
const person = new Person("田中", 25);
console.log(person.greet()); // "こんにちは、田中です。"
ここで重要なのは、Person
はあくまで設計図であり、new Person()
によって実際のオブジェクト(インスタンス)が作られることです。同じ設計図から複数のインスタンスを作ることができます。
const person1 = new Person("田中", 25);
const person2 = new Person("佐藤", 30);
console.log(person1.greet()); // "こんにちは、田中です。"
console.log(person2.greet()); // "こんにちは、佐藤です。"
プロパティの初期化
プロパティには、クラス定義の時点で初期値を設定することもできます。これは、すべてのインスタンスで共通の初期値を持たせたい場合に便利です。
class Car {
// 初期値を設定したプロパティ
brand: string = "Unknown"; // ブランドが指定されない場合の既定値
year: number = 2023; // 年式の既定値
isRunning: boolean = false; // エンジンの状態(最初は停止)
}
const car = new Car();
console.log(car.brand); // "Unknown"
console.log(car.isRunning); // false
コンストラクタの理解
コンストラクタは、インスタンスが作成される瞬間に自動的に実行される特別なメソッドです。主にプロパティの初期化を行います。
class Book {
title: string;
author: string;
pages: number;
// コンストラクタで必要な情報を受け取る
constructor(title: string, author: string, pages: number) {
// 受け取った引数を、このインスタンスのプロパティに代入
this.title = title;
this.author = author;
this.pages = pages;
}
}
// new を使ってインスタンスを作成すると、コンストラクタが実行される
const book = new Book("吾輩は猫である", "夏目漱石", 200);
TypeScript の便利な記法
上記のような「引数を受け取ってプロパティに代入する」処理は非常によく行われるため、TypeScript ではより簡潔に書ける方法が用意されています。
class Product {
// public を付けることで、プロパティの宣言と初期化が同時に行われる
constructor(
public name: string, // this.name = name; と同じ意味
public price: number, // this.price = price; と同じ意味
private id: string, // 外部からアクセスできないプロパティ
protected category: string // 継承時にアクセス可能なプロパティ
) {
// コンストラクタの中身は空でも、プロパティの初期化は自動的に行われる
}
getInfo(): string {
return `${this.name}: ¥${this.price}`;
}
}
この記法を使うと、従来の書き方と比べてコードが大幅に短くなります。
パラメータープロパティでは、引数の前に public
、private
、protected
のいずれかを付ける必要があります。何も付けないと、通常の引数として扱われ、プロパティは作成されません。
オプショナルプロパティとデフォルト値
すべての情報が必須ではない場合、オプショナルパラメータやデフォルト値を使用できます。
class User {
constructor(
public name: string, // 必須項目
public email?: string, // オプション(undefinedの可能性がある)
public age: number = 0, // デフォルト値付き
public isActive: boolean = true // デフォルト値付き
) {}
}
// 様々な作成方法が可能
const user1 = new User("山田"); // 名前のみ
const user2 = new User("佐藤", "[email protected]"); // 名前とメール
const user3 = new User("田中", undefined, 30); // 名前と年齢(メールは未設定)
const user4 = new User("鈴木", "[email protected]", 25, false); // すべて指定
オーバーロード
同じクラスでも、異なる引数パターンでインスタンスを作成したい場合があります。TypeScript では、コンストラクタオーバーロードでこれを実現できます。
class Rectangle {
width: number;
height: number;
// 引数が1つの場合:正方形を作成
constructor(size: number);
// 引数が2つの場合:長方形を作成
constructor(width: number, height: number);
// 実際の実装(上記の両方のパターンに対応する必要がある)
constructor(widthOrSize: number, height?: number) {
if (height === undefined) {
// 引数が1つの場合は正方形
this.width = widthOrSize;
this.height = widthOrSize;
} else {
// 引数が2つの場合は長方形
this.width = widthOrSize;
this.height = height;
}
}
}
const square = new Rectangle(10); // 10×10の正方形
const rect = new Rectangle(10, 20); // 10×20の長方形
コンストラクタオーバーロードを使うことで、使用者が直感的に理解できるAPIを提供できます。正方形なら1つの値、長方形なら2つの値を渡すという使い方が可能になります。
メソッドの実装
メソッドは、クラスが提供する機能を定義します。データを操作したり、計算を行ったり、他のオブジェクトとやり取りしたりします。
基本的なメソッド
class Calculator {
// 基本的な足し算メソッド
add(a: number, b: number): number {
return a + b;
}
// 引き算メソッド
subtract(a: number, b: number): number {
return a - b;
}
// メソッドオーバーロード:引数の数によって動作を変える
multiply(a: number, b: number): number;
multiply(a: number, b: number, c: number): number;
multiply(a: number, b: number, c?: number): number {
return c ? a * b * c : a * b;
}
}
const calc = new Calculator();
console.log(calc.add(5, 3)); // 8
console.log(calc.multiply(2, 3)); // 6
console.log(calc.multiply(2, 3, 4)); // 24
thisの理解
メソッド内で this
を使うことで、同じインスタンスの他のプロパティやメソッドにアクセスできます。
class BankAccount {
constructor(
public accountNumber: string,
private balance: number = 0
) {}
// 残高を確認するメソッド
getBalance(): number {
return this.balance; // このインスタンスのbalanceプロパティにアクセス
}
// お金を預けるメソッド
deposit(amount: number): void {
this.balance += amount; // このインスタンスの残高を増やす
}
// お金を引き出すメソッド
withdraw(amount: number): boolean {
if (this.balance >= amount) {
this.balance -= amount;
return true; // 引き出し成功
}
return false; // 残高不足
}
// 残高の状況を文字列で返すメソッド
getStatement(): string {
return `口座番号: ${this.accountNumber}, 残高: ¥${this.getBalance()}`;
}
}
const account = new BankAccount("12345");
account.deposit(10000);
console.log(account.getStatement()); // "口座番号: 12345, 残高: ¥10000"
アロー関数
通常のメソッドとアロー関数メソッドでは、this
の扱いが異なります。この違いを理解することは、特にイベント処理で重要です。
class EventHandler {
message: string = "Hello";
// 通常のメソッド
regularMethod() {
return this.message;
}
// アロー関数メソッド
arrowMethod = () => {
return this.message;
}
}
const handler = new EventHandler();
// 直接呼び出す場合は両方とも正常に動作
console.log(handler.regularMethod()); // "Hello"
console.log(handler.arrowMethod()); // "Hello"
// しかし、メソッドを変数に代入して呼び出すと...
const regular = handler.regularMethod;
const arrow = handler.arrowMethod;
// console.log(regular()); // エラーまたはundefined(thisが失われる)
console.log(arrow()); // "Hello"(thisが保持される)
イベントハンドラーやコールバック関数として渡す場合は、アロー関数メソッドを使用することで this
の問題を回避できます。ただし、アロー関数メソッドは継承時に上書きできないという制約があります。
ゲッターとセッター
プロパティのようにアクセスできるメソッドを作ることができます。これにより、値の取得や設定時に特別な処理を実行できます。
class Temperature {
private _celsius: number = 0; // プライベートプロパティ(直接アクセス不可)
// セッター:値を設定する時に呼ばれる
set celsius(value: number) {
this._celsius = value;
}
// ゲッター:値を取得する時に呼ばれる
get celsius(): number {
return this._celsius;
}
// 華氏温度のゲッター(摂氏から自動計算)
get fahrenheit(): number {
return (this._celsius * 9/5) + 32;
}
// 華氏温度のセッター(設定時に摂氏に変換)
set fahrenheit(value: number) {
this._celsius = (value - 32) * 5/9;
}
}
const temp = new Temperature();
temp.celsius = 25; // セッターが呼ばれる
console.log(temp.celsius); // 25(ゲッターが呼ばれる)
console.log(temp.fahrenheit); // 77(摂氏から華氏に自動変換)
temp.fahrenheit = 86; // 華氏で設定
console.log(temp.celsius); // 30(華氏から摂氏に自動変換)
ゲッターとセッターを使うことで、プロパティへのアクセスを制御し、値の妥当性チェックや自動計算などを組み込むことができます。
実践的なクラス設計例
ここまでの知識を使って、実際の開発で使えそうなクラスを作成してみます。
class Circle {
// 半径はプライベートにして、外部から直接変更されないようにする
constructor(private radius: number) {
// 半径が負の値でないかチェック
if (radius < 0) {
throw new Error("半径は0以上である必要があります");
}
}
// 面積を計算するメソッド
getArea(): number {
return Math.PI * this.radius * this.radius;
}
// 円周を計算するメソッド
getCircumference(): number {
return 2 * Math.PI * this.radius;
}
// 直径を計算するメソッド
getDiameter(): number {
return this.radius * 2;
}
// 半径を安全に変更するメソッド
setRadius(newRadius: number): void {
if (newRadius < 0) {
throw new Error("半径は0以上である必要があります");
}
this.radius = newRadius;
}
// 半径を取得するメソッド
getRadius(): number {
return this.radius;
}
// 円の情報をまとめて取得するメソッド
getInfo(): string {
return `半径: ${this.radius}, 面積: ${this.getArea().toFixed(2)}, 円周: ${this.getCircumference().toFixed(2)}`;
}
// 別の円と比較するメソッド
isLargerThan(other: Circle): boolean {
return this.getArea() > other.getArea();
}
}
// 使用例
const smallCircle = new Circle(3);
const largeCircle = new Circle(5);
console.log(smallCircle.getInfo());
console.log(largeCircle.getInfo());
console.log(largeCircle.isLargerThan(smallCircle)); // true
このクラス設計では、以下の点に注意しています
- データの整合性を保つため、半径への直接アクセスを制限
- 値の妥当性チェックを組み込み
- 関連する計算処理をメソッドとして提供
- 他のインスタンスとの比較機能を提供
まとめ
これらの基本を理解することで、保守性の高いオブジェクト指向コードを書く土台ができます。