1. ホーム
  2. TypeScript

【TypeScript】クラスのアクセス修飾子(public・private・protected)の使い方|プロパティ・メソッドの公開範囲を制御する

Share

クラスを設計するとき、「このプロパティは外から書き換えてほしくない」「このメソッドはクラスの中だけで使いたい」と思うことがあります。TypeScript では public / private / protected というアクセス修飾子を使って、プロパティやメソッドをどこから触れるかを制御できます。この記事では、3つの修飾子の違いと使い分け、コンストラクタの引数で一気にプロパティを宣言する短縮記法、継承(extends)と protected の関係、そして readonly# プライベートフィールドとの違いまで、初心者〜中級者向けに解説します。

3つの修飾子で「公開範囲」を決める

アクセス修飾子は、プロパティやメソッドの前に付けるキーワードです。付ける修飾子によって、そのメンバーにアクセスできる範囲が変わります。まずはそれぞれの意味を整理しておきましょう。

修飾子アクセスできる範囲
publicどこからでも(クラスの外・サブクラス・クラス内すべて)。省略時の既定値
protectedそのクラス自身と、それを継承したサブクラスの中だけ
privateそのクラスの中だけ。サブクラスからも外からも触れない

公開の広い順に publicprotectedprivate と覚えると分かりやすいです。何も付けなければ public になるので、修飾子は「範囲を狭めたいとき」に付けるものだと考えると自然です。

public と private の基本

まずは publicprivate の違いを、銀行口座を模したクラスで見てみます。残高は外から勝手に書き換えられると困るので private にし、入金や残高確認はメソッドを通して行うようにします。

bank-account.ts
class BankAccount {
  public owner: string;   // 外から自由に読み書きできる
  private balance: number; // クラスの中だけで扱う

  constructor(owner: string, initial: number) {
    this.owner = owner;
    this.balance = initial;
  }

  // 入金:メソッド経由でだけ残高を変更できる
  public deposit(amount: number): void {
    this.balance += amount;
  }

  // 残高確認:中の balance を安全に読み出す
  public getBalance(): number {
    return this.balance;
  }
}

const account = new BankAccount("田中", 1000);
account.deposit(500);
console.log(account.getBalance()); // 1500
console.log(account.owner);        // "田中"(public なのでOK)

// account.balance は private なのでエラーになる
// Property 'balance' is private and only accessible within class 'BankAccount'.
console.log(account.balance);

ownerpublic なのでクラスの外から account.owner と読めますが、balanceprivate なので account.balance と直接触ろうとするとコンパイルエラーになります。残高の変更は deposit()、確認は getBalance() というメソッドを通すことで、「マイナスの入金を弾く」といったルールをクラス側に集約できます。これがアクセス修飾子でデータを守る基本の考え方です。

なお TypeScript の private は「型チェックの上での制約」です。コンパイル後の JavaScript では通常のプロパティになるため、実行時に完全に隠されるわけではありません。実行時にも本当に隠したい場合は、後述の # プライベートフィールドを使います。

コンストラクタの引数でプロパティをまとめて宣言する

先ほどの例では「プロパティを宣言」→「コンストラクタで代入」という同じ名前を2回書く手間がありました。TypeScript には、コンストラクタの引数にアクセス修飾子を付けるだけで、プロパティの宣言と代入をまとめて行える短縮記法(パラメータプロパティ)があります。

parameter-properties.ts
class BankAccount {
  // 引数に修飾子を付けると、プロパティ宣言 + this への代入を自動でやってくれる
  constructor(
    public owner: string,
    private balance: number
  ) {}

  public deposit(amount: number): void {
    this.balance += amount;
  }

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

const account = new BankAccount("田中", 1000);
account.deposit(500);
console.log(account.getBalance()); // 1500

コンストラクタの引数に publicprivate を付けると、TypeScript が同名のプロパティを自動で作り、this.owner = owner のような代入も裏側で行ってくれます。最初の例と動きは同じですが、記述量がぐっと減ります。修飾子を付けた引数だけがプロパティになる点に注意してください。修飾子を付けない引数は、コンストラクタの中でだけ使える普通の引数のままです。

protected と継承の関係

protectedprivate に似ていますが、「サブクラス(継承した子クラス)からはアクセスできる」という点が違います。親クラスの内部データを子クラスにだけ使わせたいときに使います。動物クラスを親に、犬クラスを子にして見てみましょう。

animal.ts
class Animal {
  constructor(protected name: string) {}

  // サブクラスから呼べるようにするヘルパー
  protected describe(): string {
    return `${this.name} です`;
  }
}

class Dog extends Animal {
  public bark(): string {
    // protected な name と describe() は、サブクラスの中からアクセスできる
    return `${this.describe()}。ワン!`;
  }
}

const dog = new Dog("ポチ");
console.log(dog.bark()); // "ポチ です。ワン!"

// 一方、クラスの外からは protected メンバーには触れない
// Property 'name' is protected and only accessible within class 'Animal' and its subclasses.
console.log(dog.name);

DogAnimal を継承しているので、bark() の中から親の protected メンバーである namedescribe() にアクセスできます。しかし dog.name のようにクラスの外から触ろうとするとエラーになります。ここが private との決定的な違いです。もし nameprivate にしていたら、子クラスの Dog からも this.name にアクセスできず、エラーになります。

使い分けの目安は、「継承した子クラスに使わせたい内部メンバーは protected、自分のクラスだけで完結する内部メンバーは private」です。外部に公開する API だけを public にし、それ以外はできるだけ範囲を狭めておくと、後から変更しても影響範囲が読みやすくなります。

readonly を併用して書き換えも禁止する

アクセス修飾子は「どこから触れるか」を制御しますが、readonly を組み合わせると「一度決めたら書き換えられない」という制約も追加できます。両者は目的が異なるので、併用できます。

readonly-modifier.ts
class User {
  // 外から読めるが書き換え不可(public readonly)
  constructor(
    public readonly id: number,
    private name: string
  ) {}

  public rename(name: string): void {
    this.name = name; // private だが readonly ではないので変更できる
  }
}

const user = new User(1, "山田");
console.log(user.id); // 1(public なので読める)

// id は readonly なので代入しようとするとエラー
// Cannot assign to 'id' because it is a read-only property.
user.id = 2;

public readonly id は「外から読めるが、初期化後は誰も書き換えられない」プロパティです。readonly なプロパティへの代入はコンストラクタの中だけ許され、それ以外の場所からの代入はエラーになります。ID のように一度決まったら変わらない値に向いています。修飾子の順番は public readonly のように、アクセス修飾子 → readonly の順で書きます。

private と # プライベートフィールドの違い

TypeScript には private のほかに、JavaScript 標準(ECMAScript)の # を先頭に付けるプライベートフィールドもあります。どちらも「クラスの外から触れない」点は同じですが、性質が異なります。

hard-private.ts
class Counter {
  #count = 0; // ECMAScript のプライベートフィールド

  public increment(): void {
    this.#count++;
  }

  public get value(): number {
    return this.#count;
  }
}

const counter = new Counter();
counter.increment();
console.log(counter.value); // 1

// #count はクラスの外からは構文的に参照できない
// Property '#count' is not accessible outside class 'Counter'
console.log(counter.#count);

大きな違いは「隠れる強さ」です。private は TypeScript の型チェック上の制約なので、コンパイル後の JavaScript では普通のプロパティになり、実行時には外から参照できてしまいます。一方 # フィールドは JavaScript の言語仕様レベルで隠され、コンパイル後も実行時に本当にアクセスできません。実行時まで確実に隠したい場合は # が有効です。ただし # フィールドはサブクラスからもアクセスできない(protected 相当の使い方ができない)ため、継承先で使わせたい内部メンバーには protected を選ぶことになります。この記事の主題であるアクセス修飾子で十分なことが多いので、まずは public / private / protected を押さえておけば問題ありません。

修飾子を付けてもエラーにならないと感じたとき

private にしたはずのプロパティに外から触れてもエラーが出ない、という場合、いくつか原因が考えられます。

そもそも型チェックが走っていない

アクセス修飾子はコンパイル時(型チェック)にだけ効きます。.ts ファイルを tsc や型チェック付きのエディタで開いていなかったり、素の JavaScript として実行していたりすると、修飾子は無視されます。エラーはエディタ上の赤い波線や tsc --noEmit の出力で確認してください。実行時に node.js を動かしただけでは、private の違反は検出されません。

実行時にも隠したいのに private を使っている

前述のとおり private は型の上での約束事なので、コンパイル後の JavaScript では外から参照できます。実行時にも確実に隠したいなら # プライベートフィールドを使ってください。「型チェックでうっかりアクセスを防ぎたい」だけなら private で十分です。目的に合わせて選び分けるのがポイントです。

まとめ

TypeScript のアクセス修飾子は、プロパティやメソッドの公開範囲を制御する仕組みです。public はどこからでもアクセスでき(省略時の既定値)、protected はそのクラスとサブクラスの中だけ、private はそのクラスの中だけに限定されます。継承した子クラスに使わせたい内部メンバーは protected、自分のクラスで完結するものは private、外部に公開する API は public、と範囲を狭める方向で使い分けるのが基本です。コンストラクタの引数に修飾子を付ける短縮記法を使えばプロパティ宣言をまとめて書けますし、readonly を併用すれば書き換えも禁止できます。実行時まで確実に隠したい場合は # プライベートフィールドという選択肢もあることを覚えておくと、設計の幅が広がります。

参考ページ