Use when exposing internal state. Use when making fields public for convenience. Use when external code modifies object internals.
Hide internal state. Expose behavior, not data. Control access through methods.
Public fields let anyone modify your object's internals, bypassing validation and breaking invariants. Encapsulation protects data integrity.
NEVER expose internal state directly. Always use methods to control access.
No exceptions:
If internal state is directly accessible, STOP:
// ❌ VIOLATION: Public state
class BankAccount {
public balance: number = 0; // Anyone can modify!
public transactions: Transaction[] = [];
}
// Callers can break invariants
const account = new BankAccount();
account.balance = -1000000; // Negative balance!
account.transactions = []; // Audit trail destroyed!
Problems:
// ✅ CORRECT: Encapsulated state
class BankAccount {
private _balance: number = 0;
private _transactions: Transaction[] = [];
get balance(): number {
return this._balance;
}
deposit(amount: number): void {
if (amount <= 0) {
throw new Error('Deposit must be positive');
}
this._balance += amount;
this._transactions.push({
type: 'deposit',
amount,
timestamp: new Date()
});
}
withdraw(amount: number): void {
if (amount <= 0) {
throw new Error('Withdrawal must be positive');
}
if (amount > this._balance) {
throw new Error('Insufficient funds');
}
this._balance -= amount;
this._transactions.push({
type: 'withdrawal',
amount,
timestamp: new Date()
});
}
getTransactionHistory(): ReadonlyArray<Transaction> {
return [...this._transactions]; // Return copy
}
}
// Now invariants are protected
const account = new BankAccount();
account.deposit(100); // ✅ Validated, logged
account.withdraw(50); // ✅ Validated, logged
account.balance = -1000; // ❌ Error: Cannot set
class User {
private _password: string;
setPassword(newPassword: string): void {
if (newPassword.length < 8) throw new Error('Too short');
this._password = hash(newPassword);
}
checkPassword(attempt: string): boolean {
return verify(attempt, this._password);
}
}
class Config {
readonly apiUrl: string;
readonly timeout: number;
constructor(apiUrl: string, timeout: number) {
this.apiUrl = apiUrl;
this.timeout = timeout;
}
}
class Order {
private _items: OrderItem[] = [];
// ❌ BAD: Returns reference
getItems(): OrderItem[] {
return this._items; // Caller can modify!
}
// ✅ GOOD: Returns copy
getItems(): OrderItem[] {
return [...this._items];
}
// ✅ ALSO GOOD: Return readonly
getItems(): ReadonlyArray<OrderItem> {
return this._items;
}
}
class Product {
private _price: number = 0;
get price(): number {
return this._price;
}
set price(value: number) {
if (value < 0) throw new Error('Price cannot be negative');
if (value > 1000000) throw new Error('Price too high');
this._price = value;
}
}
Pressure: "Public fields are less code"
Response: Less code now, more bugs later. Encapsulation prevents invalid states.
Action: Private fields + methods. The extra code is validation.
Pressure: "Our team won't misuse public fields"
Response: Teams grow. Code evolves. Mistakes happen. Protect invariants in code.
Action: Don't rely on caller discipline. Enforce in class.
Pressure: "This class is just a data container"
Response: Even data has rules. Emails have formats. Ages have ranges.
Action: Use DTOs/interfaces for pure data. Classes = behavior + encapsulation.
Pressure: "Java-style getters/setters are boilerplate"
Response: TypeScript has concise get/set syntax. Use it.
Action: get balance() is not verbose.
public keyword on mutable fieldsAll of these mean: Encapsulate the state.
| Exposed (Bad) | Encapsulated (Good) |
|---|---|
public balance |
private _balance + deposit()/withdraw() |
return this.items |
return [...this.items] |
| Direct mutation | Method with validation |
| Trust callers | Enforce in class |
| Excuse | Reality |
|---|---|
| "Simpler" | Simpler to write, harder to maintain. |
| "We trust callers" | Code should enforce, not trust. |
| "Just data" | Data has constraints. Enforce them. |
| "Verbose" | TypeScript getters are concise. |
| "Over-engineering" | It's just engineering. |
Private state. Public methods. Validate on every change.
Never expose internal state directly. Return copies of collections. Validate in setters. Encapsulation protects invariants and enables safe evolution.