← Курс/Модификаторы доступа: public, private, protected#166 из 257+20 XP

Модификаторы доступа: public, private, protected

Три модификатора доступа

TypeScript добавляет к классам три модификатора, определяющих откуда можно обращаться к полю или методу:

class Person {
  public name: string      // доступен везде (по умолчанию)
  private age: number      // только внутри класса Person
  protected email: string  // внутри Person и его подклассов

  constructor(name: string, age: number, email: string) {
    this.name = name
    this.age = age
    this.email = email
  }

  getInfo(): string {
    return `${this.name}, ${this.age} лет`  // private доступен здесь
  }
}

const p = new Person('Алексей', 30, 'a@mail.ru')
console.log(p.name)    // OK — public
// p.age               // Ошибка TS: 'age' is private
// p.email             // Ошибка TS: 'email' is protected

class Employee extends Person {
  getEmail() {
    return this.email  // OK — protected доступен в подклассе
    // this.age        // Ошибка TS: 'age' is private (не в подклассе!)
  }
}

Краткая запись параметров конструктора

Модификаторы в конструкторе объявляют и инициализируют поле одновременно:

// Длинная запись
class UserLong {
  private name: string
  readonly id: number

  constructor(name: string, id: number) {
    this.name = name
    this.id = id
  }
}

// Краткая запись — идентична по результату
class UserShort {
  constructor(
    private name: string,
    public readonly id: number,
    protected role: string = 'user'
  ) {}
}

private vs # (ES private fields)

TypeScript поддерживает два вида приватности:

class Counter {
  private tsPrivate = 0    // TypeScript private — проверяется только компилятором
  #jsPrivate = 0           // ECMAScript private — реально приватный в runtime

  increment() {
    this.tsPrivate++
    this.#jsPrivate++
  }
}

const c = new Counter()
// c.tsPrivate   // Ошибка TS, но в скомпилированном JS доступно!
// c.#jsPrivate  // Ошибка и в TS, и в JS runtime — настоящая приватность

Используйте # если нужна гарантия приватности в runtime (не только в TypeScript).

readonly

Поле можно установить только в объявлении или конструкторе:

class Config {
  readonly maxRetries: number
  readonly apiUrl: string = 'https://api.example.com'

  constructor(maxRetries: number) {
    this.maxRetries = maxRetries   // OK — в конструкторе
  }

  update() {
    // this.maxRetries = 5  // Ошибка TS: Cannot assign to 'maxRetries'
  }
}

Практический пример: инкапсуляция состояния

class Stack<T> {
  private items: T[] = []

  push(item: T): this {       // возвращает this для цепочки
    this.items.push(item)
    return this
  }

  pop(): T | undefined {
    return this.items.pop()
  }

  get size(): number {        // только чтение снаружи
    return this.items.length
  }

  get isEmpty(): boolean {
    return this.items.length === 0
  }

  protected peek(): T | undefined {  // доступен в подклассах
    return this.items[this.items.length - 1]
  }
}

class BoundedStack<T> extends Stack<T> {
  constructor(private readonly capacity: number) {
    super()
  }

  push(item: T): this {
    if (this.size >= this.capacity) {
      throw new Error(`Стек заполнен (max: ${this.capacity})`)
    }
    return super.push(item)
  }

  top(): T | undefined {
    return this.peek()   // OK — protected
  }
}

Примеры

Инкапсуляция через # private fields: банковский счёт с приватным балансом и историей транзакций

// TypeScript private → JS # private fields (ES2022)
// Используем настоящие приватные поля # — реальная инкапсуляция в runtime

class Transaction {
  #type    // 'deposit' | 'withdrawal'
  #amount
  #timestamp
  #balanceAfter

  constructor(type, amount, balanceAfter) {
    this.#type         = type
    this.#amount       = amount
    this.#timestamp    = new Date()
    this.#balanceAfter = balanceAfter
  }

  // Только геттеры — readonly снаружи
  get type()         { return this.#type }
  get amount()       { return this.#amount }
  get timestamp()    { return this.#timestamp }
  get balanceAfter() { return this.#balanceAfter }

  toString() {
    const sign = this.#type === 'deposit' ? '+' : '-'
    const time = this.#timestamp.toTimeString().slice(0, 8)
    return `[${time}] ${sign}${this.#amount} → баланс: ${this.#balanceAfter}`
  }
}

class BankAccount {
  #balance
  #owner
  #history  // private — снаружи недоступна
  #frozen   // private — управление только через методы

  // TypeScript: constructor(private owner: string, initialBalance = 0)
  constructor(owner, initialBalance = 0) {
    if (initialBalance < 0) throw new RangeError('Начальный баланс не может быть отрицательным')
    this.#owner   = owner
    this.#balance = initialBalance
    this.#history = []
    this.#frozen  = false
  }

  // public геттеры — только чтение
  get owner()   { return this.#owner }
  get balance() { return this.#balance }
  get isFrozen() { return this.#frozen }

  deposit(amount) {
    this.#assertNotFrozen()
    this.#assertPositive(amount, 'Сумма пополнения')
    this.#balance += amount
    this.#history.push(new Transaction('deposit', amount, this.#balance))
    return this
  }

  withdraw(amount) {
    this.#assertNotFrozen()
    this.#assertPositive(amount, 'Сумма снятия')
    if (amount > this.#balance) throw new Error('Недостаточно средств')
    this.#balance -= amount
    this.#history.push(new Transaction('withdrawal', amount, this.#balance))
    return this
  }

  freeze()   { this.#frozen = true;  return this }
  unfreeze() { this.#frozen = false; return this }

  // protected аналог — только для текущего класса
  getHistory() {
    return [...this.#history]  // копия — не мутируем
  }

  getStatement() {
    return [
      `Владелец: ${this.#owner}`,
      `Баланс: ${this.#balance} руб.`,
      `Транзакций: ${this.#history.length}`,
      '',
      ...this.#history.map(t => t.toString()),
    ].join('\n')
  }

  // Приватные вспомогательные методы
  #assertNotFrozen() {
    if (this.#frozen) throw new Error('Счёт заморожен')
  }

  #assertPositive(value, label) {
    if (value <= 0) throw new RangeError(`${label} должна быть положительной`)
  }
}

// --- Демонстрация ---
const acc = new BankAccount('Алексей Петров', 10000)

acc.deposit(5000).deposit(3000).withdraw(2000)

console.log('=== История операций ===')
console.log(acc.getStatement())

console.log('\n=== Приватность ===')
// acc.#balance   // SyntaxError — реально приватное
// acc.#history   // SyntaxError — реально приватное
console.log('Баланс через геттер:', acc.balance)  // OK

console.log('\n=== Заморозка счёта ===')
acc.freeze()
console.log('Счёт заморожен:', acc.isFrozen)
try {
  acc.withdraw(100)
} catch (e) {
  console.log('Ошибка:', e.message)  // 'Счёт заморожен'
}

acc.unfreeze()
acc.withdraw(500)
console.log('Баланс после разморозки:', acc.balance)