【TypeScript】初学者向け、クラスのアクセス制御と継承

【TypeScript】初学者向け、クラスのアクセス制御と継承

クラスのメンバーへのアクセスを制御する仕組みと、既存のクラスを拡張して新しい機能を追加する継承について学習していきましょう。

Typescript #Typescript#初学者向け#継承

【TypeScript】初学者向け、クラスのアクセス制御と継承

サムネイル

クラスのメンバーへのアクセスを制御する仕組みと、既存のクラスを拡張して新しい機能を追加する継承について学習していきましょう。

更新日: 8/1/2025

アクセス修飾子

プログラムが大きくなってくると、「どの部分を外部から変更できるようにするか」「どの部分は内部でのみ使用するか」を明確に区別する必要が出てきます。

たとえば、銀行口座を表すクラスを考えてみてください。残高は外部から直接変更されてはいけませんが、残高を確認することは必要です。また、内部的な計算処理は外部に公開する必要がありません。

// 問題のあるクラス設計
class BadBankAccount {
  balance: number = 0; // 誰でも直接変更できてしまう
  
  constructor(initialBalance: number) {
    this.balance = initialBalance;
  }
}

const account = new BadBankAccount(1000);
account.balance = -500; // 直接変更できてしまう(危険)

アクセス修飾子を使うことで、こうした問題を防げます。

public

public は制限の少ないアクセス修飾子で、どこからでもアクセス可能です。何も指定しない場合の既定値でもあります。

class PublicExample {
  public name: string; // 明示的にpublicを指定
  age: number;         // publicを省略(同じ意味)
  
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  
  public getName(): string { // 明示的にpublicを指定
    return this.name;
  }
  
  getAge(): number { // publicを省略(同じ意味)
    return this.age;
  }
}

const example = new PublicExample("田中", 25);
console.log(example.name);    // アクセス可能
console.log(example.age);     // アクセス可能
console.log(example.getName()); // アクセス可能
example.name = "佐藤";        // 変更も可能

通常、外部から使用されることを想定したプロパティやメソッドには public を使用します。ただし、既定値なので明示的に書く必要はありません。

private

private メンバーは、そのクラス内からのみアクセス可能です。外部からの直接アクセスや変更を防ぎたい場合に使用します。

class PrivateExample {
  private secret: string;      // 外部からアクセス不可
  private internalId: number;  // 内部でのみ使用
  
  constructor(secret: string) {
    this.secret = secret;
    this.internalId = Math.floor(Math.random() * 1000);
  }
  
  // private メソッド(クラス内でのみ使用)
  private generateHash(): string {
    return `${this.secret}_${this.internalId}`;
  }
  
  // public メソッド(外部から使用可能)
  public getPublicInfo(): string {
    // クラス内からはprivateメンバーにアクセス可能
    const hash = this.generateHash();
    return `ハッシュ値: ${hash.substring(0, 5)}...`;
  }
}

const example = new PrivateExample("秘密のデータ");

// これらはアクセス可能
console.log(example.getPublicInfo()); // OK

// これらはコンパイルエラーになる
// console.log(example.secret);       // エラー:privateプロパティにアクセス不可
// console.log(example.generateHash()); // エラー:privateメソッドにアクセス不可
ISSUE - 課題

private メンバーは TypeScript のコンパイル時にのみチェックされます。JavaScript にコンパイルされた後は、実際にはアクセス可能になってしまいます。真の意味での非公開にしたい場合は、ES2022 の # 記法を使用してください。

protected

protected メンバーは、そのクラスと継承先のクラスからアクセス可能です。継承を前提とした設計で、親クラスの内部構造を子クラスに公開したい場合に使用します。

class Animal {
  protected species: string;  // 継承クラスからアクセス可能
  private internalData: string = "内部データ"; // 継承クラスからもアクセス不可
  
  constructor(species: string) {
    this.species = species;
  }
  
  // protected メソッド(継承クラスから使用可能)
  protected makeSound(): string {
    return "何らかの音";
  }
  
  // public メソッド(外部から使用可能)
  public introduce(): string {
    return `私は${this.species}です`;
  }
}

class Dog extends Animal {
  constructor() {
    super("犬"); // 親クラスのコンストラクタを呼び出し
  }
  
  public bark(): string {
    // 継承クラスからはprotectedメンバーにアクセス可能
    const sound = this.makeSound(); // OK
    return `${this.species}が${sound}を発しています`; // OK
    
    // しかしprivateメンバーにはアクセスできない
    // return this.internalData; // エラー
  }
}

const dog = new Dog();
console.log(dog.introduce()); // "私は犬です"
console.log(dog.bark());      // "犬が何らかの音を発しています"

// 外部からはprotectedメンバーにアクセスできない
// console.log(dog.species);    // エラー
// console.log(dog.makeSound()); // エラー

protected は継承における親クラスと子クラス間での情報共有に使用されます。外部からは隠蔽しつつ、継承関係にあるクラス間では共有したいデータや処理に使用します。

readonly

readonly 修飾子を使用すると、プロパティの読み取り専用化ができます。一度設定されると、その後は変更できません。

class ImmutableData {
  readonly id: number;           // 読み取り専用
  readonly createdAt: Date;      // 読み取り専用
  public name: string;           // 通常のプロパティ(変更可能)
  
  constructor(id: number, name: string) {
    // コンストラクタ内でのみ readonly プロパティに値を設定できる
    this.id = id;
    this.createdAt = new Date();
    this.name = name;
  }
  
  updateName(newName: string): void {
    this.name = newName; // 通常のプロパティは変更可能
    
    // readonly プロパティは変更できない
    // this.id = 999;       // エラー
    // this.createdAt = new Date(); // エラー
  }
}

const data = new ImmutableData(1, "初期データ");
console.log(data.id);        // 1(読み取り可能)
data.updateName("更新データ"); // 通常のプロパティは変更可能

// 外部からも readonly プロパティは変更できない
// data.id = 2;         // エラー
// data.createdAt = new Date(); // エラー

readonly プロパティは、作成時に決まって以降変更されることのないデータ(ID、作成日時、設定値など)に適用します。こうすることで、意図しない変更を防ぎ、コードの安全性を高められます。

継承の基本概念

継承は、既存のクラス(親クラス)を元にして新しいクラス(子クラス)を作成する仕組みです。子クラスは親クラスの機能を受け継ぎ、さらに独自の機能を追加できます。

なぜ継承が必要なのでしょうか。たとえば、車とトラックを表すクラスを作るとします。どちらも「ブランド」「年式」「エンジンを始動する」という共通の特徴を持ちますが、トラックには「荷物を積む」という独自の機能があります。

// 継承を使わない場合(コードの重複が発生)
class Car {
  brand: string;
  year: number;
  
  constructor(brand: string, year: number) {
    this.brand = brand;
    this.year = year;
  }
  
  start(): string {
    return `${this.brand}のエンジンを始動しました`;
  }
}

class Truck {
  brand: string;     // Carと重複
  year: number;      // Carと重複
  loadCapacity: number;
  
  constructor(brand: string, year: number, loadCapacity: number) {
    this.brand = brand;    // Carと重複
    this.year = year;      // Carと重複
    this.loadCapacity = loadCapacity;
  }
  
  start(): string {        // Carと重複
    return `${this.brand}のエンジンを始動しました`;
  }
  
  loadCargo(weight: number): string {
    return `${weight}kgの荷物を積みました`;
  }
}

継承を使うことで、このコードの重複を解決できます。

基本的な継承

// 親クラス(基底クラス)
class Vehicle {
  protected brand: string;  // 子クラスからもアクセス可能
  protected year: number;
  
  constructor(brand: string, year: number) {
    this.brand = brand;
    this.year = year;
  }
  
  start(): string {
    return `${this.brand}のエンジンを始動しました`;
  }
  
  getInfo(): string {
    return `${this.year}年製の${this.brand}`;
  }
}

// 子クラス(派生クラス)
class Car extends Vehicle {
  private doors: number; // 車特有のプロパティ
  
  constructor(brand: string, year: number, doors: number) {
    // super() で親クラスのコンストラクタを呼び出す(必須)
    super(brand, year);
    this.doors = doors;
  }
  
  // 親クラスのメソッドを上書き(オーバーライド)
  start(): string {
    // super.メソッド名() で親クラスのメソッドを呼び出せる
    return `${super.start()}(車)`;
  }
  
  // 子クラス独自のメソッド
  openDoors(): string {
    return `${this.doors}つのドアを開けました`;
  }
}

class Truck extends Vehicle {
  private loadCapacity: number; // トラック特有のプロパティ
  
  constructor(brand: string, year: number, loadCapacity: number) {
    super(brand, year);
    this.loadCapacity = loadCapacity;
  }
  
  start(): string {
    return `${super.start()}(トラック)`;
  }
  
  loadCargo(weight: number): string {
    if (weight > this.loadCapacity) {
      return `積載重量を超えています(最大: ${this.loadCapacity}kg)`;
    }
    return `${weight}kgの荷物を積みました`;
  }
}

// 使用例
const car = new Car("トヨタ", 2023, 4);
const truck = new Truck("いすゞ", 2022, 5000);

console.log(car.start());        // "トヨタのエンジンを始動しました(車)"
console.log(car.getInfo());      // "2023年製のトヨタ"(親クラスのメソッド)
console.log(car.openDoors());    // "4つのドアを開けました"(子クラス独自)

console.log(truck.start());      // "いすゞのエンジンを始動しました(トラック)"
console.log(truck.loadCargo(3000)); // "3000kgの荷物を積みました"

継承における重要なポイント

1. super() の呼び出しは必須
子クラスのコンストラクタでは、最初に super() を呼び出して親クラスのコンストラクタを実行する必要があります。

2. メソッドのオーバーライド
子クラスで同名のメソッドを定義すると、親クラスのメソッドを上書きできます。super.メソッド名() で親クラスの元のメソッドも呼び出せます。

3. protected の活用
親クラスのプロパティを protected にすることで、子クラスからアクセスできるようにしつつ、外部からは隠蔽できます。

多層継承

継承は複数の階層にわたって行えます。生物の分類のような階層構造を表現する場合に有用です。

// 最上位クラス
class LivingBeing {
  protected name: string;
  
  constructor(name: string) {
    this.name = name;
  }
  
  live(): string {
    return `${this.name}は生きています`;
  }
}

// 中間クラス
class Animal extends LivingBeing {
  protected species: string;
  
  constructor(name: string, species: string) {
    super(name); // 親クラス(LivingBeing)のコンストラクタを呼び出し
    this.species = species;
  }
  
  move(): string {
    return `${this.name}は動き回っています`;
  }
}

// 最下位クラス
class Mammal extends Animal {
  private furColor: string;
  
  constructor(name: string, species: string, furColor: string) {
    super(name, species); // 親クラス(Animal)のコンストラクタを呼び出し
    this.furColor = furColor;
  }
  
  nurse(): string {
    return `${this.name}は子どもに授乳しています`;
  }
  
  getFullInfo(): string {
    // 全階層のメソッドを組み合わせて使用
    return `${this.live()}, ${this.move()}, ${this.nurse()}`;
  }
}

const dog = new Mammal("ポチ", "犬", "茶色");
console.log(dog.getFullInfo());
// "ポチは生きています, ポチは動き回っています, ポチは子どもに授乳しています"

静的メンバー

静的メンバーは、クラスのインスタンスを作成せずに直接アクセスできるメンバーです。インスタンス間で共有される情報や、インスタンスに依存しないユーティリティ機能を提供する際に使用します。

静的プロパティとメソッド

class MathUtils {
  // 静的定数(全インスタンスで共有される)
  static readonly PI = 3.14159;
  static readonly E = 2.71828;
  
  // 静的メソッド(インスタンス作成不要で使用可能)
  static add(a: number, b: number): number {
    return a + b;
  }
  
  static multiply(a: number, b: number): number {
    return a * b;
  }
  
  static circleArea(radius: number): number {
    // 静的メソッド内では、同じクラスの静的メンバーにアクセス可能
    return MathUtils.PI * radius * radius;
  }
  
  // 静的メソッド内ではインスタンスメンバーにはアクセスできない
  // static invalidMethod(): void {
  //   console.log(this.someInstanceProperty); // エラー
  // }
}

// インスタンス化不要で使用可能
console.log(MathUtils.PI);              // 3.14159
console.log(MathUtils.add(5, 3));       // 8
console.log(MathUtils.circleArea(10));  // 314.159

// インスタンスを作成しても静的メンバーにはアクセスできない
const math = new MathUtils();
// console.log(math.PI); // エラー:静的プロパティはインスタンスからアクセス不可

静的初期化ブロック

クラスが初めて読み込まれた時に実行される初期化処理を定義できます。

class DatabaseConfig {
  static connection: string;
  static isInitialized: boolean = false;
  static supportedDatabases: string[] = [];
  
  // 静的初期化ブロック(クラス読み込み時に一度だけ実行)
  static {
    console.log("データベース設定を初期化中...");
    
    // 環境変数から設定を読み込み
    this.connection = process.env.DB_URL || "localhost:5432";
    
    // サポートするデータベースの一覧を設定
    this.supportedDatabases = ["PostgreSQL", "MySQL", "SQLite"];
    
    this.isInitialized = true;
    console.log("初期化完了");
  }
  
  static getConnection(): string {
    if (!this.isInitialized) {
      throw new Error("設定が初期化されていません");
    }
    return this.connection;
  }
  
  static isSupported(database: string): boolean {
    return this.supportedDatabases.includes(database);
  }
}

// クラスが最初に参照された時に初期化ブロックが実行される
console.log(DatabaseConfig.getConnection()); // 初期化ログが表示された後、接続文字列が返される

組み合わせ技

インスタンス固有の情報と、全インスタンス共通の情報を組み合わせて使用する例を見てみましょう。

class Counter {
  // 全インスタンス共通のカウンタ
  private static totalCount: number = 0;
  
  // インスタンス固有のカウンタ
  private instanceCount: number = 0;
  
  // インスタンス識別用のID
  private readonly id: number;
  
  constructor() {
    // インスタンス作成時に全体カウンタを増やす
    Counter.totalCount++;
    this.id = Counter.totalCount; // 作成順序をIDとして使用
  }
  
  // インスタンスメソッド
  increment(): void {
    this.instanceCount++;        // このインスタンスのカウンタを増やす
    Counter.totalCount++;        // 全体のカウンタも増やす
  }
  
  getInstanceCount(): number {
    return this.instanceCount;
  }
  
  getId(): number {
    return this.id;
  }
  
  // 静的メソッド
  static getTotalCount(): number {
    return Counter.totalCount;
  }
  
  static reset(): void {
    Counter.totalCount = 0;
    // 注意:既存のインスタンスのIDには影響しない
  }
  
  // インスタンスメソッドから静的メンバーにアクセス
  getGlobalRatio(): string {
    const globalCount = Counter.getTotalCount();
    const ratio = (this.instanceCount / globalCount * 100).toFixed(1);
    return `このインスタンスは全体の${ratio}%を占めています`;
  }
}

// 使用例
const counter1 = new Counter(); // ID: 1, totalCount: 1
const counter2 = new Counter(); // ID: 2, totalCount: 2

counter1.increment(); // counter1: 1, totalCount: 3
counter1.increment(); // counter1: 2, totalCount: 4
counter2.increment(); // counter2: 1, totalCount: 5

console.log(`Counter1 (ID: ${counter1.getId()}): ${counter1.getInstanceCount()}`); // 2
console.log(`Counter2 (ID: ${counter2.getId()}): ${counter2.getInstanceCount()}`); // 1
console.log(`Total: ${Counter.getTotalCount()}`); // 5

console.log(counter1.getGlobalRatio()); // "このインスタンスは全体の40.0%を占めています"

静的メンバーを使用することで、インスタンス間で共有されるデータや、インスタンスに依存しないユーティリティ機能を提供できます。ただし、静的メンバーは継承時に子クラスでも共有されるため、設計時に注意が必要です。

アクセス制御の例

これまでの知識を組み合わせて、銀行口座管理システムのような実際の業務で使えるクラス設計を考えてみましょう。

class BankAccount {
  // 全口座で共有される情報
  private static totalAccounts: number = 0;
  private static readonly bankName: string = "サンプル銀行";
  
  // インスタンス固有の情報
  private readonly accountNumber: string;
  private balance: number;
  private readonly createdAt: Date;
  protected accountHolder: string; // 継承クラスからもアクセス可能
  
  constructor(accountHolder: string, initialBalance: number = 0) {
    // 入力値の検証
    if (initialBalance < 0) {
      throw new Error("初期残高は0以上である必要があります");
    }
    
    // アカウント情報の設定
    BankAccount.totalAccounts++;
    this.accountNumber = `ACC${BankAccount.totalAccounts.toString().padStart(6, '0')}`;
    this.accountHolder = accountHolder;
    this.balance = initialBalance;
    this.createdAt = new Date();
  }
  
  // 残高確認(外部から読み取り専用でアクセス可能)
  getBalance(): number {
    return this.balance;
  }
  
  // 口座情報取得(外部から基本情報のみ取得可能)
  getAccountInfo(): object {
    return {
      accountNumber: this.accountNumber,
      accountHolder: this.accountHolder,
      balance: this.balance,
      bankName: BankAccount.bankName
    };
  }
  
  // 入金処理
  deposit(amount: number): boolean {
    if (amount <= 0) {
      console.log("入金額は0より大きい必要があります");
      return false;
    }
    
    this.balance += amount;
    console.log(`¥${amount}を入金しました。残高: ¥${this.balance}`);
    return true;
  }
  
  // 出金処理
  withdraw(amount: number): boolean {
    if (amount <= 0) {
      console.log("出金額は0より大きい必要があります");
      return false;
    }
    
    if (this.balance < amount) {
      console.log("残高不足です");
      return false;
    }
    
    this.balance -= amount;
    console.log(`¥${amount}を出金しました。残高: ¥${this.balance}`);
    return true;
  }
  
  // 内部処理(継承クラスからも使用可能)
  protected validateAmount(amount: number): boolean {
    return amount > 0 && amount <= 1000000; // 一度に100万円まで
  }
  
  // 静的メソッド(銀行全体の情報)
  static getTotalAccounts(): number {
    return BankAccount.totalAccounts;
  }
  
  static getBankName(): string {
    return BankAccount.bankName;
  }
}

// 継承例:貯蓄口座
class SavingsAccount extends BankAccount {
  private interestRate: number;
  
  constructor(accountHolder: string, initialBalance: number = 0, interestRate: number = 0.01) {
    super(accountHolder, initialBalance);
    this.interestRate = interestRate;
  }
  
  // 利息計算(このクラス独自の機能)
  calculateInterest(): number {
    return this.getBalance() * this.interestRate;
  }
  
  // 利息の追加
  addInterest(): void {
    const interest = this.calculateInterest();
    if (interest > 0) {
      // 親クラスのprotectedメソッドを使用
      if (this.validateAmount(interest)) {
        // 直接balanceを変更するのではなく、depositメソッドを使用
        this.deposit(interest);
        console.log(`利息¥${interest}が追加されました`);
      }
    }
  }
  
  // 親クラスのメソッドをオーバーライド
  getAccountInfo(): object {
    const basicInfo = super.getAccountInfo(); // 親クラスの情報を取得
    return {
      ...basicInfo,
      accountType: "貯蓄口座",
      interestRate: this.interestRate
    };
  }
}

// 使用例
const account1 = new BankAccount("田中太郎", 50000);
const savings1 = new SavingsAccount("佐藤花子", 100000, 0.02);

account1.deposit(10000);
account1.withdraw(5000);

savings1.addInterest();
console.log(savings1.getAccountInfo());

console.log(`${BankAccount.getBankName()}の総口座数: ${BankAccount.getTotalAccounts()}`);

この設計では、以下のアクセス制御を実現しています。

  • private: 残高など、外部から直接変更されては困る情報
  • protected: 継承クラスで使用したい内部ロジック
  • public: 外部に提供するAPI
  • readonly: 作成後に変更されない情報(口座番号、作成日時)
  • static: 銀行全体で共有される情報

まとめ

publicは基本的にはつけません。

検索

検索条件に一致する記事が見つかりませんでした