Модификаторы доступа

Модификаторы доступа контролируют видимость свойств и методов класса: public, private, protected, readonly и #private fields.

Зачем нужно

  • Инкапсуляция — скрытие внутренней реализации от внешнего кода
  • Защита данных от некорректного изменения
  • Чёткий API класса — что доступно снаружи, а что нет
  • readonly — защита от случайного изменения

Где используется

  • Любой класс с инкапсуляцией
  • Сервисы, репозитории — private для внутреннего состояния
  • Наследование — protected для доступа из наследников
  • DTO и value objects — readonly для неизменяемых данных

Предпосылки

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 Только чтение

Частые ошибки

  1. private не защищает в рантайме — используйте #private для настоящей приватности
  2. readonly объектов — readonly свойство-объект можно мутировать внутри
class Config {
  readonly options: { debug: boolean } = { debug: false };
}

const config = new Config();
config.options = { debug: true }; // Ошибка! readonly
config.options.debug = true;      // OK! Мутация внутри объекта
  1. protected в конструкторе — protected constructor не позволяет создавать экземпляр напрямую
  2. Parameter properties без модификатора — обычный параметр (без public/private/protected/readonly) не создаёт свойство
class Bad {
  constructor(name: string) {} // name — просто параметр, не свойство!
  // this.name — ошибка
}

class Good {
  constructor(public name: string) {} // name — свойство класса
}

Практика

  1. Создайте BankAccount с private balance и public deposit/withdraw/getBalance
  2. Реализуйте наследование с protected методами
  3. Сравните private и #private — попробуйте обойти каждый через as any
  4. Создайте immutable value object с readonly свойствами
  5. Используйте parameter properties для сокращения кода конструктора

Связанные темы

Ресурсы