TypeScriptにおいて、クラスのメンバ(プロパティやメソッド)に対するアクセス制限を適切に設定することは、堅牢なアプリケーションを開発する上で非常に重要です。
アクセス修飾子を正しく活用することで、オブジェクト指向プログラミングの基本原則である「カプセル化」を実現し、予期せぬバグの混入を防ぐことができます。
本記事では、TypeScriptで利用可能なpublic、private、protectedの各修飾子の違いと、実務で役立つ実装ルールについて詳しく紹介します。
TypeScriptにおけるアクセス修飾子の役割
アクセス修飾子とは、クラスの外部や継承先のクラスから、そのクラスのプロパティやメソッドにどこまでアクセスを許可するかを制御するための仕組みです。
大規模な開発プロジェクトでは、多くの開発者が同じコードベースを共有するため、意図しない場所からデータを書き換えられるリスクを最小限に抑える必要があります。
TypeScriptのアクセス修飾子を適切に使い分けることで、「公開すべきインターフェース」と「隠蔽すべき内部ロジック」を明確に分離することが可能になります。
これにより、コードの可読性が向上するだけでなく、将来的な仕様変更の際にも影響範囲を特定しやすくなるというメリットがあります。
public(公開):デフォルトのアクセスレベル
TypeScriptにおいて、アクセス修飾子を明示的に指定しない場合、すべてのメンバは自動的にpublicとして扱われます。
public修飾子が付与されたメンバは、クラスの内部、インスタンスを介した外部、そして継承したサブクラスのどこからでも自由にアクセスできます。
以下に、publicを使用した基本的なコード例を示します。
class User {
// 明示的にpublicを指定(省略しても同じ挙動になります)
public name: string;
constructor(name: string) {
this.name = name;
}
public greet(): void {
console.log(`こんにちは、${this.name}さん。`);
}
}
const user = new User("田中");
// クラスの外部からプロパティにアクセス可能
console.log(user.name);
// クラスの外部からメソッドを呼び出し可能
user.greet();
田中
こんにちは、田中さん。
実務においては、コードの意図を明確にするために、あえてpublicを省略せずに記述するスタイルを採用するチームも存在します。
しかし、冗長さを避けるために「デフォルトがpublicである」という前提で省略することが一般的です。
private(非公開):クラス内部のみに制限
private修飾子は、そのメンバが定義されたクラスの内部からのみアクセスを許可します。
クラスの外部や、そのクラスを継承したサブクラスからであっても、privateメンバにアクセスしようとするとコンパイルエラーが発生します。
これは、クラスの内部状態を保護し、外部から不正な書き換えを行わせないために最も強力な手段となります。
class BankAccount {
// 外部から直接書き換えられないようにprivateを設定
private balance: number;
constructor(initialBalance: number) {
this.balance = initialBalance;
}
public deposit(amount: number): void {
if (amount > 0) {
this.balance += amount;
console.log(`${amount}円入金しました。`);
}
}
public getBalance(): void {
console.log(`現在の残高は${this.balance}円です。`);
}
}
const account = new BankAccount(1000);
account.deposit(500);
account.getBalance();
// 以下はエラーになります
// console.log(account.balance); // Property 'balance' is private and only accessible within class 'BankAccount'.
500円入金しました。
現在の残高は1500円です。
このように、内部的な数値や状態管理ロジックはprivateで隠蔽し、操作用のメソッドのみを公開するのが実務的な設計の基本です。
なお、TypeScriptのprivateはコンパイル時のチェックであり、JavaScriptに変換された後は通常のプロパティとして振る舞う点に注意が必要です。
もし実行時(ランタイム)でも完全に隠蔽したい場合は、ECMAScript標準のプライベートフィールドである#を使用することを検討してください。
protected(保護):継承先までアクセスを許可
protected修飾子は、privateと似ていますが、「継承関係にあるサブクラス内」からのアクセスを許可する点が異なります。
クラスのインスタンス(外部)からはアクセスさせたくないが、機能を拡張した子クラスではその変数やメソッドを使いたい場合に利用します。
class Animal {
// 継承先でも利用できるようにprotectedを設定
protected species: string;
constructor(species: string) {
this.species = species;
}
}
class Dog extends Animal {
constructor() {
super("犬");
}
public introduce(): void {
// 親クラスのprotectedメンバにアクセス可能
console.log(`私は${this.species}です。`);
}
}
const myDog = new Dog();
myDog.introduce();
// 以下はエラーになります
// console.log(myDog.species); // Property 'species' is protected and only accessible within class 'Animal' and its subclasses.
私は犬です。
protectedは、テンプレートメソッドパターンのような設計でよく使われます。
親クラスで共通のロジックを定義し、具体的な実装の詳細はサブクラスに委ねつつ、共通のプロパティを共有したい場合に非常に便利です。
アクセス修飾子の比較表
各修飾子のアクセス範囲をまとめると以下の通りになります。
| 修飾子 | クラス内部 | 子クラス内部 | インスタンス(外部) |
|---|---|---|---|
| public | ○ 許可 | ○ 許可 | ○ 許可 |
| protected | ○ 許可 | ○ 許可 | × 禁止 |
| private | ○ 許可 | × 禁止 | × 禁止 |
実務で役立つ実装ルールとテクニック
readonly修飾子との組み合わせ
アクセス修飾子と併せてよく使われるのがreadonly修飾子です。
readonlyを指定すると、コンストラクタ以外での値の代入を禁止することができます。
例えば、public readonlyとすることで、「外部から読み取りは可能だが、書き換えは不可」という安全なプロパティを定義できます。
class Config {
// 外部から参照はできるが、変更はさせない
public readonly apiKey: string;
constructor(key: string) {
this.apiKey = key;
}
}
const config = new Config("secret-key-123");
console.log(config.apiKey);
// config.apiKey = "new-key"; // エラー: Cannot assign to 'apiKey' because it is a read-only property.
Parameter Propertiesによる記述の簡略化
TypeScriptには、コンストラクタの引数にアクセス修飾子を記述することで、プロパティの宣言と初期化を同時に行うParameter Propertiesという便利な機能があります。
これにより、クラス定義を劇的に短縮することができます。
// 冗長な書き方
class Employee long {
private id: number;
constructor(id: number) {
this.id = id;
}
}
// Parameter Propertiesを使ったスマートな書き方
class Employee {
// 引数にアクセス修飾子を付けるだけでプロパティとして定義される
constructor(private id: number, public name: string, protected role: string) {}
public showId(): void {
console.log(`ID: ${this.id}`);
}
}
const emp = new Employee(1, "佐藤", "エンジニア");
emp.showId();
実務ではこのParameter Propertiesが多用されます。
コードが非常にスッキリし、プロパティの定義漏れや代入忘れを防ぐことができるため、積極的に活用しましょう。
原則としての使い分け指針
アクセス修飾子の選定に迷った際は、「最も制限の厳しいものから検討する」という原則に従うのが良いでしょう。
まず全てのプロパティをprivateとし、必要に応じてprotectedやpublicへ緩和していくアプローチです。
最初からすべてをpublicにしてしまうと、どこでその変数が変更されているかを追跡するのが困難になり、リファクタリングの難易度が上がってしまいます。
TypeScriptのprivate vs JavaScriptの#
モダンなTypeScript開発において、プライベートメンバの実装には2通りの方法があります。
1つは本記事で解説したprivateキーワード、もう1つはJavaScriptの標準仕様である#(ハッシュ)プレフィックスです。
両者の違いを理解しておくことで、プロジェクトに最適な選択ができます。
| 機能 | private 修飾子 | # プライベートフィールド |
|---|---|---|
| チェック時期 | コンパイル時のみ | 実行時(ランタイム)でも有効 |
| 構文 | private property | #property |
| パフォーマンス | 通常のプロパティと同等 | 若干のオーバーヘッドがある場合がある |
| 互換性 | 古いJS環境でも動作 | ES2015以降が必要(トランスパイル設定による) |
実務においては、TypeScriptの強力な型チェックを活かせるprivate修飾子が一般的に利用されます。
しかし、ライブラリ開発などでユーザーがJavaScript環境からアクセスしてくる可能性がある場合は、実行時にも確実に隠蔽できる#の使用が推奨されます。
まとめ
TypeScriptのアクセス修飾子は、クラスの設計品質を高め、チーム開発を円滑にするための強力なツールです。
public、private、protectedの各特性を理解し、適切に使い分けることで、バグに強くメンテナンス性の高いコードを実現できます。
また、readonlyやParameter PropertiesといったTypeScript独自の機能と組み合わせることで、より簡潔で安全なクラス定義が可能になります。
まずは「不要なものは公開しない」という最小権限の原則を意識して、日々のコーディングに取り入れてみてください。
適切なカプセル化は、将来のあなたやチームメンバーを助ける大きな資産となるはずです。
