はじめてのTypeScriptプログラミング入門 30日コース | Generics・Utility Types・Class・Module - Day 26:Class

TypeScript 30日で身につけるTypeScript
スポンサーリンク
スポンサーリンク

Day 26:Classで「銀行口座」を設計してみる

第4週のDay 26では、いよいよ Class(クラス) に入っていきます。 オブジェクト指向の入り口として、とても定番で分かりやすい題材を使います。

銀行口座クラスを作り、 deposit() / withdraw() / getBalance() を実装する。

扱うキーワードは次のとおりです。

  • Class
  • Constructor
  • Property
  • Method
  • public
  • private
  • protected
  • readonly

「銀行口座」という身近なイメージを使いながら、 クラスの基本をステップバイステップでかみ砕いていきます。

Classとは何かをざっくりイメージする

「設計図」と「実体」の関係

クラスは、よく「設計図」にたとえられます。

  • クラス:銀行口座の設計図
  • インスタンス:実際の銀行口座(1つ1つの口座)

TypeScriptのクラスは、次のような要素で構成されます。

  • Property(プロパティ):クラスが持つ「状態」
  • Method(メソッド):クラスが持つ「振る舞い(処理)」
  • Constructor(コンストラクタ):インスタンスを作るときに呼ばれる「初期化処理」

これを銀行口座に当てはめると、

  • Property:口座番号、名義、残高
  • Method:入金(deposit)、出金(withdraw)、残高確認(getBalance)
  • Constructor:口座を作るときに初期残高や名義を設定する

というイメージになります。

Step 1:銀行口座クラスの骨組みを作る

まずは、最低限の骨組みから始めます。

// 銀行口座クラスの基本形
class BankAccount {
  // プロパティ(状態)
  private balance: number; // 残高は外から直接触らせたくないのでprivateにする

  // コンストラクタ(初期化)
  constructor(initialBalance: number) {
    this.balance = initialBalance;
  }

  // メソッド(振る舞い)
  deposit(amount: number): void {
    // 入金処理
  }

  withdraw(amount: number): void {
    // 出金処理
  }

  getBalance(): number {
    // 残高を返す
    return this.balance;
  }
}
TypeScript

ここではまだ中身を空にしていますが、 クラスの構造としてはすでに「銀行口座っぽい形」になっています。

Step 2:Propertyとアクセス修飾子(public / private / protected / readonly)

Propertyとは

プロパティは、クラスが持つ「データ(状態)」です。

銀行口座なら、たとえば次のようなものがあります。

  • 口座番号
  • 名義
  • 残高

アクセス修飾子の役割

TypeScriptのクラスでは、プロパティやメソッドに 次のようなアクセス修飾子を付けることができます。

  • public
    • どこからでもアクセス可能(デフォルト)
  • private
    • クラスの内部からしかアクセスできない
  • protected
    • クラス自身と、そのサブクラスからアクセスできる
  • readonly
    • 一度代入したら変更できない(読み取り専用)

銀行口座の「残高」は、外から勝手に書き換えられると困ります。 そのため、private にしてクラスの外から直接触れないようにします。

また、「口座番号」は一度決まったら変わらないものなので、 readonly にするのが自然です。

銀行口座クラスにプロパティを追加する

class BankAccount {
  // 口座番号(読み取り専用)
  public readonly accountNumber: string;

  // 名義(外から参照はできるが、変更はメソッド経由にしたい場合はprivateでもよい)
  public ownerName: string;

  // 残高(外から直接変更されたくないのでprivate)
  private balance: number;

  constructor(accountNumber: string, ownerName: string, initialBalance: number) {
    this.accountNumber = accountNumber;
    this.ownerName = ownerName;
    this.balance = initialBalance;
  }

  // ここにメソッドを追加していく
}
TypeScript

重要ポイント:

  • readonly accountNumber
    • コンストラクタで一度値を設定したら、その後は変更できません。
  • private balance
    • クラスの外から account.balance = 0 のような操作はできなくなります。
    • 残高の変更は必ず deposit / withdraw を通じて行われるようになります。

Step 3:deposit() を実装する(入金)

入金メソッド deposit() は、 「指定された金額を残高に足す」処理です。

ただし、現実的には「マイナスの金額を入金しようとしたらエラー」などのチェックも必要です。

class BankAccount {
  public readonly accountNumber: string;
  public ownerName: string;
  private balance: number;

  constructor(accountNumber: string, ownerName: string, initialBalance: number) {
    this.accountNumber = accountNumber;
    this.ownerName = ownerName;
    this.balance = initialBalance;
  }

  // 入金メソッド
  public deposit(amount: number): void {
    if (amount <= 0) {
      // 0以下の金額は入金として受け付けない
      throw new Error("入金額は正の数である必要があります");
    }

    this.balance += amount;
  }

  // 出金メソッド(後で実装)
  public withdraw(amount: number): void {
    // ...
  }

  // 残高取得メソッド
  public getBalance(): number {
    return this.balance;
  }
}
TypeScript

ポイント:

  • public deposit(amount: number): void
    • メソッドもアクセス修飾子を付けられます。
    • ここでは外から呼び出せるように public にしています。
  • this.balance += amount;
    • balance は private ですが、クラスの内部からはアクセスできます。
    • 外からは account.balance と書けないので、 「残高の変更は必ずこのメソッドを通る」というルールを守れます。

Step 4:withdraw() を実装する(出金)

出金メソッド withdraw() は、 「指定された金額を残高から引く」処理です。

ここでも、現実的なチェックが必要です。

  • マイナスの金額は出金として受け付けない
  • 残高より多い金額は引き出せない
class BankAccount {
  public readonly accountNumber: string;
  public ownerName: string;
  private balance: number;

  constructor(accountNumber: string, ownerName: string, initialBalance: number) {
    this.accountNumber = accountNumber;
    this.ownerName = ownerName;
    this.balance = initialBalance;
  }

  public deposit(amount: number): void {
    if (amount <= 0) {
      throw new Error("入金額は正の数である必要があります");
    }
    this.balance += amount;
  }

  // 出金メソッド
  public withdraw(amount: number): void {
    if (amount <= 0) {
      throw new Error("出金額は正の数である必要があります");
    }

    if (amount > this.balance) {
      throw new Error("残高不足です");
    }

    this.balance -= amount;
  }

  public getBalance(): number {
    return this.balance;
  }
}
TypeScript

重要ポイント:

  • 「残高不足」をチェックしているところが、 クラスの中に「銀行口座のルール」を閉じ込めている部分です。
  • 外からは balance に直接触れないので、 「ルールを無視して残高をマイナスにする」ことができません。

Step 5:getBalance() を実装する(残高確認)

残高確認メソッド getBalance() は、 「現在の残高を返すだけ」のシンプルなメソッドです。

public getBalance(): number {
  return this.balance;
}
TypeScript

これにより、外からは次のように使えます。

const account = new BankAccount("123-456", "Taro", 1000);

console.log("現在の残高:", account.getBalance()); // 1000

account.deposit(500);
console.log("入金後の残高:", account.getBalance()); // 1500

account.withdraw(300);
console.log("出金後の残高:", account.getBalance()); // 1200
TypeScript

ポイント:

  • 残高は private ですが、 getBalance() を通じて「読み取りだけ」外に公開しています。
  • 「書き込みはメソッド経由」「読み取りは専用メソッド経由」という形にすることで、 クラスの外からの操作をきれいに制御できます。

Step 6:銀行口座クラスの完成形と使用例

ここまでの内容をひとつにまとめた、 練習用テンプレートを載せます。

// day26-bank-account.ts
// Day 26:Classで銀行口座を作る

class BankAccount {
  // 口座番号(読み取り専用)
  public readonly accountNumber: string;

  // 名義(今回はpublicにしておく)
  public ownerName: string;

  // 残高(外から直接変更されたくないのでprivate)
  private balance: number;

  // コンストラクタ:口座番号・名義・初期残高を受け取る
  constructor(accountNumber: string, ownerName: string, initialBalance: number) {
    this.accountNumber = accountNumber;
    this.ownerName = ownerName;
    this.balance = initialBalance;
  }

  // 入金メソッド
  public deposit(amount: number): void {
    if (amount <= 0) {
      throw new Error("入金額は正の数である必要があります");
    }
    this.balance += amount;
  }

  // 出金メソッド
  public withdraw(amount: number): void {
    if (amount <= 0) {
      throw new Error("出金額は正の数である必要があります");
    }

    if (amount > this.balance) {
      throw new Error("残高不足です");
    }

    this.balance -= amount;
  }

  // 残高取得メソッド
  public getBalance(): number {
    return this.balance;
  }
}

// ===== 使用例 =====

const account = new BankAccount("123-456", "Taro", 1000);

console.log("口座番号:", account.accountNumber); // 123-456
console.log("名義:", account.ownerName);         // Taro
console.log("初期残高:", account.getBalance());  // 1000

account.deposit(500);
console.log("500円入金後の残高:", account.getBalance()); // 1500

account.withdraw(300);
console.log("300円出金後の残高:", account.getBalance()); // 1200

// 残高不足の例
try {
  account.withdraw(5000);
} catch (e) {
  console.error("エラー:", (e as Error).message); // 残高不足です
}

// 直接残高をいじろうとするとどうなるか?
// account.balance = 0; // コンパイルエラー:privateなのでアクセス不可
TypeScript

Day 26のまとめ

Day 26では、

  • Class(クラス)を「設計図」として捉えること
  • Constructor / Property / Method の役割
  • public / private / readonly を使って、 「どこから何を触れるか」を型で制御する考え方
  • 銀行口座クラス BankAccount を題材に、 deposit() / withdraw() / getBalance() を実装する流れ

を、具体的なコードとコメント付きで見てきました。

クラスは、「データ」と「そのデータに対するルール」を ひとつのまとまりとして表現するための仕組みです。

今日の銀行口座クラスをベースに、

  • 「ログ出力用のメソッドを追加する」
  • 「出金時に手数料を引く」
  • 「protectedを使って、サブクラスで振る舞いを拡張する」

といった発展も、少しずつ試してみてください。 クラスの世界が、ぐっと立体的に見えてくるはずです。


Day 26:4つのClassで「オブジェクト指向の感覚」をつかむ

Day 26の演習では、Classの考え方をぐっと身近にするために、 次の4つのクラスを題材にしていきます。

  • User Class
  • Product Class
  • Cart Class
  • BankAccount Class

キーワードは変わらずこちらです。

  • Class
  • Constructor
  • Property
  • Method
  • public
  • private
  • protected
  • readonly

「ユーザー」「商品」「カート」「銀行口座」という、 WebアプリやECサイトでよく登場する世界観を使いながら、 クラス設計の基本をステップバイステップでかみ砕いていきます。

Classの基本をもう一度整理する

Classは「設計図」、インスタンスは「実体」

クラスはよく「設計図」にたとえられます。

  • Class:UserやProductの「型」と振る舞いの設計図
  • インスタンス:実際のユーザー1人、商品1つ、カート1つ

クラスの中には、主に次の要素があります。

  • Property(プロパティ):クラスが持つ「状態」
  • Method(メソッド):クラスが持つ「振る舞い(処理)」
  • Constructor(コンストラクタ):インスタンス生成時の「初期化処理」

さらに、アクセス修飾子で「どこから触れるか」を制御します。

  • public:どこからでもアクセス可能
  • private:クラスの内部からのみアクセス可能
  • protected:クラス自身とサブクラスからアクセス可能
  • readonly:一度代入したら変更不可

これらをうまく組み合わせることで、 「安全で意図が伝わるクラス設計」ができるようになります。

User Class:ユーザー情報をクラスで表現する

どんなプロパティとメソッドが必要か考える

Userクラスには、たとえば次のような情報と振る舞いが考えられます。

  • プロパティ
    • id(ユーザーID)
    • name(名前)
    • email(メールアドレス)
  • メソッド
    • changeEmail()(メールアドレス変更)
    • greet()(挨拶メッセージを返す)

id は一度決まったら変わらないことが多いので readonly にするのが自然です。

User Classの実装例

// Userクラス:ユーザー情報を表現する
class User {
  // ユーザーID(読み取り専用)
  public readonly id: number;

  // 名前(外から参照・変更可能)
  public name: string;

  // メールアドレス(外から参照可能だが、変更はメソッド経由にしたい場合はprivateでもよい)
  private email: string;

  // コンストラクタ:Userインスタンスを初期化する
  constructor(id: number, name: string, email: string) {
    this.id = id;
    this.name = name;
    this.email = email;
  }

  // メールアドレスを変更するメソッド
  public changeEmail(newEmail: string): void {
    // ここで形式チェックなどを入れることもできます
    this.email = newEmail;
  }

  // 現在のメールアドレスを取得するメソッド
  public getEmail(): string {
    return this.email;
  }

  // 挨拶メッセージを返すメソッド
  public greet(): string {
    return `こんにちは、${this.name}さん!`;
  }
}

// 使用例
const user = new User(1, "Taro", "[email protected]");
console.log(user.greet());              // こんにちは、Taroさん!
console.log(user.getEmail());           // [email protected]
user.changeEmail("[email protected]");
console.log(user.getEmail());           // [email protected]
TypeScript

ポイント:

  • readonly id にすることで、「IDを勝手に書き換えられない」安全性を確保しています。
  • email を private にして、変更は changeEmail() 経由にすることで、 「メールアドレス変更のルール」をクラスの中に閉じ込められます。

Product Class:商品情報をクラスで表現する

商品に必要な情報と振る舞いを考える

Productクラスには、次のようなものが考えられます。

  • プロパティ
    • id(商品ID)
    • name(商品名)
    • price(価格)
  • メソッド
    • getPriceWithTax()(税込価格を計算する)

Product Classの実装例

// Productクラス:商品情報を表現する
class Product {
  public readonly id: number;
  public name: string;
  public price: number;

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

  // 税込価格を計算するメソッド(例として10%の消費税)
  public getPriceWithTax(taxRate: number = 0.1): number {
    return Math.floor(this.price * (1 + taxRate));
  }
}

// 使用例
const book = new Product(100, "TypeScript入門", 2000);
console.log("商品名:", book.name);                     // TypeScript入門
console.log("税抜価格:", book.price);                 // 2000
console.log("税込価格:", book.getPriceWithTax());     // 2200(10%想定)
TypeScript

ポイント:

  • getPriceWithTax() のようなメソッドにロジックを閉じ込めることで、 「税込価格の計算方法」を一箇所に集約できます。
  • 価格の計算方法が変わっても、このメソッドを直せばよく、 呼び出し側のコードはそのまま使い続けられます。

Cart Class:商品をまとめて管理するクラス

カートに必要なものを整理する

Cartクラスは、「複数の商品をまとめて管理する」役割を持ちます。

  • プロパティ
    • items(カート内の商品一覧)
  • メソッド
    • addProduct()(商品を追加する)
    • removeProduct()(商品を削除する)
    • getTotal()(合計金額を計算する)

ここではシンプルに、「Productの配列」を内部に持つ形で実装してみます。

Cart Classの実装例

// Cartクラス:商品をまとめて管理する
class Cart {
  // カート内の商品一覧(外から直接いじられたくないのでprivate)
  private items: Product[] = [];

  // 商品を追加するメソッド
  public addProduct(product: Product): void {
    this.items.push(product);
  }

  // 商品を削除するメソッド(idで削除)
  public removeProduct(productId: number): void {
    this.items = this.items.filter((item) => item.id !== productId);
  }

  // カート内の商品一覧を取得するメソッド
  public getItems(): Product[] {
    // 外から直接配列をいじられないようにコピーを返す
    return [...this.items];
  }

  // 合計金額を計算するメソッド(税抜)
  public getTotal(): number {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }

  // 合計金額(税込)を計算するメソッド
  public getTotalWithTax(taxRate: number = 0.1): number {
    return this.items.reduce(
      (sum, item) => sum + item.getPriceWithTax(taxRate),
      0
    );
  }
}

// 使用例
const cart = new Cart();
const book = new Product(100, "TypeScript入門", 2000);
const pen = new Product(101, "ペン", 200);

cart.addProduct(book);
cart.addProduct(pen);

console.log("カート内の商品:", cart.getItems().map((p) => p.name)); // ["TypeScript入門", "ペン"]
console.log("合計金額(税抜):", cart.getTotal());                  // 2200
console.log("合計金額(税込):", cart.getTotalWithTax());           // 2420(10%想定)

cart.removeProduct(101);
console.log("削除後の合計金額(税抜):", cart.getTotal());          // 2000
TypeScript

重要ポイント:

  • items を private にすることで、 「カートの中身の変更は必ずメソッド経由」というルールを守れます。
  • getItems() でコピーを返しているのは、 外から items を直接書き換えられないようにするためです。

BankAccount Class:お金を扱うクラスで「安全性」を意識する

銀行口座に必要なものを整理する

BankAccountクラスは、Day 26の本編でも扱った題材です。 ここでは、他のクラスとのつながりを意識しながら、もう一度整理してみます。

  • プロパティ
    • accountNumber(口座番号)
    • ownerName(名義)
    • balance(残高)
  • メソッド
    • deposit()(入金)
    • withdraw()(出金)
    • getBalance()(残高確認)

BankAccount Classの実装例

// BankAccountクラス:銀行口座を表現する
class BankAccount {
  // 口座番号(読み取り専用)
  public readonly accountNumber: string;

  // 名義
  public ownerName: string;

  // 残高(外から直接変更されたくないのでprivate)
  private balance: number;

  constructor(accountNumber: string, ownerName: string, initialBalance: number) {
    this.accountNumber = accountNumber;
    this.ownerName = ownerName;
    this.balance = initialBalance;
  }

  // 入金メソッド
  public deposit(amount: number): void {
    if (amount <= 0) {
      throw new Error("入金額は正の数である必要があります");
    }
    this.balance += amount;
  }

  // 出金メソッド
  public withdraw(amount: number): void {
    if (amount <= 0) {
      throw new Error("出金額は正の数である必要があります");
    }

    if (amount > this.balance) {
      throw new Error("残高不足です");
    }

    this.balance -= amount;
  }

  // 残高取得メソッド
  public getBalance(): number {
    return this.balance;
  }
}

// 使用例
const account = new BankAccount("123-456", "Taro", 1000);

console.log("口座番号:", account.accountNumber); // 123-456
console.log("名義:", account.ownerName);         // Taro
console.log("初期残高:", account.getBalance());  // 1000

account.deposit(500);
console.log("500円入金後の残高:", account.getBalance()); // 1500

account.withdraw(300);
console.log("300円出金後の残高:", account.getBalance()); // 1200

try {
  account.withdraw(5000);
} catch (e) {
  console.error("エラー:", (e as Error).message); // 残高不足です
}

// account.balance = 0; // コンパイルエラー:privateなのでアクセス不可
TypeScript

ポイント:

  • お金を扱うクラスでは、private や readonly を使って 「勝手に書き換えられない」「ルールを必ず通る」設計にすることがとても重要です。
  • こうした「安全性の意識」は、セキュリティや信頼性にも直結します。

4つのClassをまとめた練習テンプレート

最後に、今回の4つのクラスをひとつのファイルにまとめた 練習用テンプレートを載せます。 このままコピーして、プロパティやメソッドを増やしたり、 protected を使った継承を試したりしながら、 クラス設計の感覚を育ててみてください。

// day26-classes-practice.ts
// Day 26:User / Product / Cart / BankAccount クラス演習

// ===== User Class =====
class User {
  public readonly id: number;
  public name: string;
  private email: string;

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

  public changeEmail(newEmail: string): void {
    this.email = newEmail;
  }

  public getEmail(): string {
    return this.email;
  }

  public greet(): string {
    return `こんにちは、${this.name}さん!`;
  }
}

// ===== Product Class =====
class Product {
  public readonly id: number;
  public name: string;
  public price: number;

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

  public getPriceWithTax(taxRate: number = 0.1): number {
    return Math.floor(this.price * (1 + taxRate));
  }
}

// ===== Cart Class =====
class Cart {
  private items: Product[] = [];

  public addProduct(product: Product): void {
    this.items.push(product);
  }

  public removeProduct(productId: number): void {
    this.items = this.items.filter((item) => item.id !== productId);
  }

  public getItems(): Product[] {
    return [...this.items];
  }

  public getTotal(): number {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }

  public getTotalWithTax(taxRate: number = 0.1): number {
    return this.items.reduce(
      (sum, item) => sum + item.getPriceWithTax(taxRate),
      0
    );
  }
}

// ===== BankAccount Class =====
class BankAccount {
  public readonly accountNumber: string;
  public ownerName: string;
  private balance: number;

  constructor(accountNumber: string, ownerName: string, initialBalance: number) {
    this.accountNumber = accountNumber;
    this.ownerName = ownerName;
    this.balance = initialBalance;
  }

  public deposit(amount: number): void {
    if (amount <= 0) {
      throw new Error("入金額は正の数である必要があります");
    }
    this.balance += amount;
  }

  public withdraw(amount: number): void {
    if (amount <= 0) {
      throw new Error("出金額は正の数である必要があります");
    }

    if (amount > this.balance) {
      throw new Error("残高不足です");
    }

    this.balance -= amount;
  }

  public getBalance(): number {
    return this.balance;
  }
}

// ===== 簡単な動作確認 =====
const user = new User(1, "Taro", "[email protected]");
console.log(user.greet());

const book = new Product(100, "TypeScript入門", 2000);
const pen = new Product(101, "ペン", 200);

const cart = new Cart();
cart.addProduct(book);
cart.addProduct(pen);
console.log("カート合計(税抜):", cart.getTotal());
console.log("カート合計(税込):", cart.getTotalWithTax());

const account = new BankAccount("123-456", "Taro", 1000);
account.deposit(500);
account.withdraw(300);
console.log("口座残高:", account.getBalance());
TypeScript

Day 26演習のまとめ

今回の演習では、

  • User / Product / Cart / BankAccount という4つのクラスを通して、 Class / Constructor / Property / Method の基本を確認しつつ、 public / private / readonly を使った「安全な設計」を体験しました。
  • 特に、
    • 「外から触ってほしくないものは private にする」
    • 「一度決まったら変わらないものは readonly にする」 といった考え方は、実務でもそのまま役立つ重要なポイントです。

この4つのクラスをベースに、

  • Userに「権限」や「ステータス」を追加する
  • Productに「在庫数」や「カテゴリ」を追加する
  • Cartに「ユーザー紐づけ」や「クーポン適用」を追加する
  • BankAccountに「履歴ログ」や「手数料計算」を追加する

など、少しずつ機能を広げていくと、 クラス設計の楽しさと奥行きが、自然と見えてくるはずです。

タイトルとURLをコピーしました