Skip to main content

Модифікатори доступу в TypeScript: public, private, protected, readonly

Модифікатори доступу в TypeScript (public, private, protected, readonly) контролюють, яким частинам класу можна звертатись ззовні, з підкласів або лише зсередини самого класу.

Теорія

TL;DR

  • Усі члени класу за замовчуванням public, тому модифікатори здебільшого обмежують доступ, а не надають його
  • private замикає член лише в межах класу; protected відкриває також підкласам; readonly дозволяє читати будь-де, але блокує перезапис після ініціалізації
  • Ці перевірки існують тільки на етапі компіляції. У скомпільованому JavaScript від них не лишається сліду
  • Практичне правило: починай з private, послаблюй до protected лише коли потрібна спадковість, і став readonly на все, що не повинно змінюватись після створення об'єкта

Швидкий приклад

typescript
class Animal { public name: string; // Доступно звідусіль private secret: string = "hidden"; // Тільки цей клас protected species: string = "mammal"; // Цей клас і підкласи readonly id: number; // Читати скрізь, записати один раз constructor(name: string, id: number) { this.name = name; this.id = id; } } const cat = new Animal("Whiskers", 1); console.log(cat.name); // "Whiskers" - public, все добре console.log(cat.id); // 1 - readonly, все добре // cat.secret // Помилка: private // cat.id = 2; // Помилка: readonly

public і readonly доступні ззовні класу. private і protected - ні.

Головна різниця

private, protected і readonly обмежують різні речі. private обмежує хто може читати або записувати член класу (лише сам клас, і більше ніхто). protected розширює це правило до підкласів. readonly взагалі не обмежує читання. Він обмежує коли можна писати: тільки під час ініціалізації. Ці дві ідеї незалежні: можна мати private readonly властивість, яка закрита для класу і заморожена після конструктора.

Коли що використовувати

  • Внутрішній стан, до якого ніхто ззовні не повинен звертатись: private
  • Допоміжний метод або властивість, яку підклас має успадкувати: protected
  • ID, URL конфігурації, будь-що що не може змінюватись після створення: readonly
  • Все, що є частиною публічного API: public (або просто без модифікатора, бо за замовчуванням public)

Таблиця порівняння

МодифікаторВсередині класуПідкласЗзовніПерезапис після ініт.
publicтактактактак
protectedтактакнітак
privateтакнінітак
readonlyодин разтільки читаннятільки читанняні

Як компілятор це обробляє

TypeScript перевіряє ці обмеження під час tsc компіляції. Компілятор сканує оголошення класів і позначає будь-який доступ, що порушує вказаний модифікатор. Після цього у скомпільованому .js файлі від них не лишається нічого. V8 і Node.js бачать звичайні поля об'єктів. Тобто private balance = 1000 стає просто balance = 1000 у вихідному коді. TypeScript Language Server у VS Code застосовує ті самі правила в реальному часі через .d.ts метадані, саме тому редактор підкреслює порушення ще до запуску.

Це відрізняється від Java чи C#, де контроль доступу перевіряється під час виконання на рівні VM. У TypeScript модифікатори це інструмент для зручної розробки. Якщо хтось зробить as any, перевірка просто зникне. Більшість розробників усвідомлюють це вперше, коли логують нібито приватну властивість і бачать її прямо у консолі.

Типові помилки

Помилка 1: вважати, що private захищає дані під час виконання

typescript
class Safe { private pin = 1234; } const s = new Safe(); (s as any).pin = 0000; // Працює. Жодної помилки. console.log((s as any).pin); // 0

private зникає при компіляції. Для реального приховування під час виконання використовуй синтаксис # JavaScript (ES2022):

typescript
class Safe { #pin = 1234; // V8 сам це контролює - доступ ззовні заблокований навіть через casting }

Помилка 2: використовувати protected замість "майже private"

typescript
class Parent { protected secret = "internal"; } class Child extends Parent { expose() { return this.secret; } } const c = new Child(); c.expose(); // "internal" - дані потрапили назовні

Якщо жодному підкласу не потрібен прямий доступ, використовуй private. protected це конкретно для хуків (hooks) спадковості, а не для "трохи менш приватного".

Помилка 3: вважати, що readonly блокує глибоку мутацію

typescript
class Config { readonly options = { debug: false }; } const cfg = new Config(); cfg.options = { debug: true }; // Помилка - перезапис посилання заблокований cfg.options.debug = true; // Ок - мутація самого об'єкта дозволена

readonly блокує перезапис посилання, але не мутацію самого об'єкта. Для глибокої незмінності комбінуй з Object.freeze() або DeepReadonly.

Помилка 4: забути синтаксис властивостей параметрів

typescript
// Ці два класи поводяться однаково: class User { private name: string; constructor(name: string) { this.name = name; } } class User { constructor(private name: string) {} // Оголошує і присвоює водночас }

Якщо забути додати модифікатор у сигнатуру конструктора, властивість просто не буде оголошена на класі.

Де зустрічається

  • React: readonly на типах пропсів для відловлювання спроб мутації всередині компонента
  • Express/Passport.js: private для обробників токенів і внутрішнього стану middleware
  • NestJS: protected методи в базових контролерах, які розширюються підкласами
  • Redux Toolkit: readonly на зрізах стану для відловлювання випадкових мутацій
  • Prisma: readonly на згенерованих ID моделей

Питання на співбесіді

Q: Що TypeScript private компілює в JavaScript?
A: Звичайне поле класу без жодних обмежень. class X { private y = 1 } стає class X { y = 1 } у вихідному коді. Будь-який доступ під час виконання працює нормально.

Q: Чи може підклас звертатись до private члена?
A: Ні. TypeScript дасть помилку компіляції. Використовуй protected, якщо підклас потребує доступу до цього члена.

Q: Яка різниця між private і readonly?
A: Вони контролюють різні речі. private визначає, хто може звертатись. readonly визначає, коли можна записувати. private readonly властивість поєднує обидва: доступ лише в межах класу і незмінність після конструктора.

Q: Яка користь від private, якщо він не діє під час виконання?
A: Зручність під час розробки. IDE підсвічує порушення ще до запуску коду, tsc блокує невірні білди, і це передає наміри команді, яка читає код.

Q: Бібліотека постачається JS-споживачам. Як реально обмежити доступ під час виконання?
A: Комбінуй підходи. TypeScript private для підтримки IDE і типів. JavaScript # поля для реального обмеження під час виконання: V8 блокує доступ незалежно від casting. Для додаткової ізоляції використовуй фабричні функції, що повертають типізований інтерфейс, щоб споживачі взагалі не тримали прямого посилання на екземпляр класу.

Приклади

Усі чотири модифікатори в одному класі

typescript
class BankAccount { public owner: string; // Доступно всім private balance: number; // Тільки всередині protected accountType: string; // Підкласи бачать readonly id: string; // Один раз встановлено, не змінюється constructor(owner: string, initial: number, id: string) { this.owner = owner; this.balance = initial; this.accountType = "standard"; this.id = id; } public deposit(amount: number): void { this.balance += amount; // OK всередині класу } public getBalance(): number { return this.balance; } } const acc = new BankAccount("Alice", 500, "ACC-001"); acc.owner; // "Alice" acc.getBalance(); // 500 // acc.balance; // Помилка: private // acc.id = "X"; // Помилка: readonly

balance прихований, але доступний через getBalance(). Це стандартний патерн інкапсуляції для класів.

Спадковість з protected

typescript
class Vehicle { protected speed: number = 0; protected accelerate(amount: number): void { this.speed += amount; } } class Car extends Vehicle { public drive(): string { this.accelerate(60); // OK - protected, підклас може викликати return `Speed: ${this.speed}km/h`; } } const car = new Car(); car.drive(); // "Speed: 60km/h" // car.speed; // Помилка: protected, ззовні ієрархії недоступно

protected тут правильний вибір: Car потребує внутрішніх деталей Vehicle, але споживачі Car не повинні їх бачити.

TypeScript private vs JavaScript #private

typescript
class Secure { private tsField = "typescript only"; #jsField = "runtime enforced"; // ES2022 reveal() { console.log(this.tsField); // OK console.log(this.#jsField); // OK } } const s = new Secure(); s.reveal(); (s as any).tsField; // Працює - TypeScript private зникає під час виконання // s.#jsField; // SyntaxError - V8 сам це блокує

Синтаксис # контролює сам V8. TypeScript private не дає жодного захисту після запуску коду. Для чутливих даних правильний вибір - #.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?