Модификаторы доступа
Модификаторы доступа контролируют видимость свойств и методов класса: public, private, protected, readonly и #private fields.
Зачем нужно
- Инкапсуляция — скрытие внутренней реализации от внешнего кода
- Защита данных от некорректного изменения
- Чёткий API класса — что доступно снаружи, а что нет
- readonly — защита от случайного изменения
Где используется
- Любой класс с инкапсуляцией
- Сервисы, репозитории — private для внутреннего состояния
- Наследование — protected для доступа из наследников
- DTO и value objects — readonly для неизменяемых данных
Предпосылки
- Классы — базовые классы TypeScript
public (по умолчанию)
Доступен везде — снаружи, внутри, в наследниках:
class User {
public name: string; // Явный public
email: string; // Неявный public (по умолчанию)
constructor(name: string, email: string) {
this.name = name;
this.email = email;
}
public greet: string { // Явный public
return `Hi, ${this.name}`;
}
}
const user = new User("Alice", "a@b.com");
user.name; // OK
user.email; // OK
user.greet; // OK
private — только внутри класса
class BankAccount {
private balance: number;
constructor(initialBalance: number) {
this.balance = initialBalance;
}
deposit(amount: number): void {
this.validateAmount(amount);
this.balance += amount;
}
withdraw(amount: number): void {
this.validateAmount(amount);
if (amount > this.balance) {
throw new Error("Insufficient funds");
}
this.balance -= amount;
}
getBalance: number {
return this.balance;
}
private validateAmount(amount: number): void {
if (amount <= 0) {
throw new Error("Amount must be positive");
}
}
}
const account = new BankAccount(1000);
account.deposit(500); // OK
account.getBalance; // OK: 1500
account.balance; // Ошибка! Property 'balance' is private
account.validateAmount(100); // Ошибка! Method is private
private — только проверка компилятора
const account = new BankAccount(1000);
// TypeScript не позволит:
account.balance; // Ошибка компиляции
// НО в JavaScript (рантайм) — доступ есть!
(account as any).balance; // 1000 — обход через any
#private fields (ECMAScript private)
Настоящие приватные поля — недоступны даже через as any:
class SecureAccount {
#balance: number;
#pin: string;
constructor(balance: number, pin: string) {
this.#balance = balance;
this.#pin = pin;
}
#validate(pin: string): boolean {
return this.#pin === pin;
}
withdraw(amount: number, pin: string): void {
if (!this.#validate(pin)) {
throw new Error("Invalid PIN");
}
this.#balance -= amount;
}
getBalance(pin: string): number {
if (!this.#validate(pin)) throw new Error("Invalid PIN");
return this.#balance;
}
}
const acc = new SecureAccount(1000, "1234");
acc.#balance; // Ошибка! Private field
(acc as any).#balance; // Ошибка! Настоящий private — не обойти
private vs #private
| Критерий | private (TS) |
#field (ES) |
|---|---|---|
| Проверка при компиляции | Да | Да |
| Защита в рантайме | Нет | Да |
Обход через as any |
Возможен | Невозможен |
| Доступ через reflection | Да | Нет |
| Наследование | Скрыт, но не конфликтует | Полностью изолирован |
| Требования к target | Любой | ES2015+ |
protected — внутри класса и наследников
class Animal {
protected name: string;
protected speed: number = 0;
constructor(name: string) {
this.name = name;
}
protected accelerate(delta: number): void {
this.speed += delta;
}
run: void {
this.accelerate(10);
console.log(`${this.name} runs at ${this.speed} km/h`);
}
}
class Horse extends Animal {
gallop: void {
// OK — protected доступен в наследнике
this.accelerate(30);
console.log(`${this.name} gallops at ${this.speed} km/h`);
}
}
const horse = new Horse("Spirit");
horse.gallop; // OK
horse.run; // OK (public)
horse.name; // Ошибка! protected
horse.accelerate(5); // Ошибка! protected
protected constructor — только наследование
class BaseService {
protected constructor(protected apiUrl: string) {}
protected async fetch<T>(endpoint: string): Promise<T> {
const res = await fetch(`${this.apiUrl}${endpoint}`);
return res.json();
}
}
// Нельзя создать напрямую:
// const service = new BaseService("/api"); // Ошибка!
class UserService extends BaseService {
constructor {
super("/api/users");
}
async getUsers: Promise<User> {
return this.fetch<User>("/");
}
}
const service = new UserService(); // OK
readonly
Свойство можно присвоить только при объявлении или в конструкторе:
class Config {
readonly apiUrl: string;
readonly timeout: number;
readonly debug: boolean = false; // Инициализация при объявлении
constructor(apiUrl: string, timeout: number) {
this.apiUrl = apiUrl; // OK — в конструкторе
this.timeout = timeout; // OK — в конструкторе
}
updateUrl(url: string): void {
this.apiUrl = url; // Ошибка! Cannot assign to 'apiUrl' because it is a read-only property
}
}
const config = new Config("https://api.example.com", 5000);
config.apiUrl = "new-url"; // Ошибка! readonly
Комбинирование модификаторов
class User {
constructor(
public readonly id: number, // public + readonly
private readonly email: string, // private + readonly
protected readonly role: string // protected + readonly
) {}
}
Parameter Properties (сводка)
class User {
constructor(
public name: string, // public свойство
private password: string, // private свойство
protected role: string, // protected свойство
public readonly id: number, // public readonly свойство
private readonly _email: string // private readonly свойство
) {}
}
// Эквивалент без parameter properties:
class User {
public name: string;
private password: string;
protected role: string;
public readonly id: number;
private readonly _email: string;
constructor(name: string, password: string, role: string, id: number, email: string) {
this.name = name;
this.password = password;
this.role = role;
this.id = id;
this._email = email;
}
}
Сводная таблица доступа
| Модификатор | Внутри класса | Наследник | Снаружи |
|---|---|---|---|
public |
Да | Да | Да |
protected |
Да | Да | Нет |
private |
Да | Нет | Нет |
#private |
Да | Нет | Нет |
readonly |
Только в constructor | — | Только чтение |
Частые ошибки
- private не защищает в рантайме — используйте
#privateдля настоящей приватности - readonly объектов — readonly свойство-объект можно мутировать внутри
class Config {
readonly options: { debug: boolean } = { debug: false };
}
const config = new Config();
config.options = { debug: true }; // Ошибка! readonly
config.options.debug = true; // OK! Мутация внутри объекта
- protected в конструкторе — protected constructor не позволяет создавать экземпляр напрямую
- Parameter properties без модификатора — обычный параметр (без public/private/protected/readonly) не создаёт свойство
class Bad {
constructor(name: string) {} // name — просто параметр, не свойство!
// this.name — ошибка
}
class Good {
constructor(public name: string) {} // name — свойство класса
}
Практика
- Создайте
BankAccountс private balance и public deposit/withdraw/getBalance - Реализуйте наследование с protected методами
- Сравните
privateи#private— попробуйте обойти каждый черезas any - Создайте immutable value object с
readonlyсвойствами - Используйте parameter properties для сокращения кода конструктора
Связанные темы
- Классы — базовые классы
- Абстрактные классы — abstract + модификаторы
- Декораторы — метаданные
- Интерфейсы — контракты (без модификаторов)