Модифікатори доступу в TypeScript: public, private, protected, readonly
Модифікатори доступу в TypeScript (public, private, protected, readonly) контролюють, яким частинам класу можна звертатись ззовні, з підкласів або лише зсередини самого класу.
Теорія
TL;DR
- Усі члени класу за замовчуванням
public, тому модифікатори здебільшого обмежують доступ, а не надають його privateзамикає член лише в межах класу;protectedвідкриває також підкласам;readonlyдозволяє читати будь-де, але блокує перезапис після ініціалізації- Ці перевірки існують тільки на етапі компіляції. У скомпільованому JavaScript від них не лишається сліду
- Практичне правило: починай з
private, послаблюй доprotectedлише коли потрібна спадковість, і ставreadonlyна все, що не повинно змінюватись після створення об'єкта
Швидкий приклад
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; // Помилка: readonlypublic і 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 захищає дані під час виконання
class Safe {
private pin = 1234;
}
const s = new Safe();
(s as any).pin = 0000; // Працює. Жодної помилки.
console.log((s as any).pin); // 0private зникає при компіляції. Для реального приховування під час виконання використовуй синтаксис # JavaScript (ES2022):
class Safe {
#pin = 1234; // V8 сам це контролює - доступ ззовні заблокований навіть через casting
}Помилка 2: використовувати protected замість "майже private"
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 блокує глибоку мутацію
class Config {
readonly options = { debug: false };
}
const cfg = new Config();
cfg.options = { debug: true }; // Помилка - перезапис посилання заблокований
cfg.options.debug = true; // Ок - мутація самого об'єкта дозволенаreadonly блокує перезапис посилання, але не мутацію самого об'єкта. Для глибокої незмінності комбінуй з Object.freeze() або DeepReadonly.
Помилка 4: забути синтаксис властивостей параметрів
// Ці два класи поводяться однаково:
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. Для додаткової ізоляції використовуй фабричні функції, що повертають типізований інтерфейс, щоб споживачі взагалі не тримали прямого посилання на екземпляр класу.
Приклади
Усі чотири модифікатори в одному класі
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"; // Помилка: readonlybalance прихований, але доступний через getBalance(). Це стандартний патерн інкапсуляції для класів.
Спадковість з protected
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
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 не дає жодного захисту після запуску коду. Для чутливих даних правильний вибір - #.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.