Skip to main content

Public vs Private Access

Introduction​

One of the core principles of object-oriented programming is encapsulation – hiding the internal details of a class and only exposing what's necessary. This makes code more maintainable, prevents bugs, and provides a clear interface for using classes.

TypeScript provides powerful access modifiers to control who can access properties and methods:

  • public - Accessible everywhere (default)
  • private - Accessible only within the class
  • protected - Accessible within the class and its subclasses
  • readonly - Property can be read but not modified after initialization

Why encapsulation matters:

  • Prevents misuse - Users can't accidentally break internal state
  • Enables refactoring - You can change internals without affecting external code
  • Enforces contracts - Clear separation between public API and private implementation
  • Improves debugging - Fewer places where bugs can originate
  • Better IntelliSense - IDEs can suggest only the relevant public members

The Problem Without Access Control​

Without access modifiers (dangerous):

class BankAccount {
balance: number;

constructor(initialBalance: number) {
this.balance = initialBalance;
}
}

let account = new BankAccount(1000);
account.balance = -5000; // Oops! Nothing stops this
console.log(account.balance); // -5000 (invalid state!)

With access modifiers (safe):

class BankAccount {
private balance: number;

constructor(initialBalance: number) {
this.balance = initialBalance;
}

deposit(amount: number): void {
if (amount > 0) {
this.balance += amount;
}
}

getBalance(): number {
return this.balance;
}
}

let account = new BankAccount(1000);
// account.balance = -5000; // ❌ Error: Property 'balance' is private
account.deposit(500); // ✅ Controlled access
console.log(account.getBalance()); // 1500

Concepts​

Public Members (Default)​

In TypeScript, class members are public by default . Public members can be accessed from anywhere:

class Person {
// Explicitly public (optional, since it's the default)
public name: string;
age: number; // Implicitly public

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

public greet(): string {
return `Hi, I'm ${this.name}`;
}
}

let person = new Person("Alice", 30);
console.log(person.name); // ✅ Accessible
console.log(person.age); // ✅ Accessible
console.log(person.greet()); // ✅ Accessible
person.name = "Bob"; // ✅ Can modify

When to use public:

  • Properties and methods that are part of the class's public API
  • Data that external code needs to access
  • Methods that define the class's behavior

Private Members​

Private members can only be accessed within the class itself. They're invisible to external code:

class BankAccount {
private balance: number; // Private field
public owner: string; // Public field

constructor(owner: string, initialBalance: number) {
this.owner = owner;
this.balance = initialBalance;
}

// Public method
public deposit(amount: number): void {
if (amount > 0) {
this.balance += amount; // ✅ Can access private balance internally
console.log(`Deposited $${amount}`);
}
}

// Public method
public getBalance(): number {
return this.balance; // ✅ Can access private balance internally
}

// Private method
private logTransaction(action: string, amount: number): void {
console.log(`[${new Date().toISOString()}] ${action}: $${amount}`);
}
}

let account = new BankAccount("Alice", 1000);

console.log(account.owner); // ✅ Public - works
account.deposit(500); // ✅ Public method - works
console.log(account.getBalance()); // ✅ Public method - works

// ❌ Errors - private members not accessible
// console.log(account.balance);
// account.balance = 5000;
// account.logTransaction("Test", 100);

When to use private:

  • Internal implementation details
  • Data that should be modified only through controlled methods
  • Helper methods that external code shouldn't call
  • Sensitive information (passwords, tokens, etc.)

Protected Members​

Protected members are accessible within the class and its subclasses , but not from outside:

class Animal {
protected name: string; // Protected - accessible in subclasses
private age: number; // Private - NOT accessible in subclasses

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

protected makeSound(): string {
return "Some sound";
}

public describe(): string {
return `${this.name} is ${this.age} years old`;
}
}

class Dog extends Animal {
private breed: string;

constructor(name: string, age: number, breed: string) {
super(name, age);
this.breed = breed;
}

public bark(): string {
// ✅ Can access protected members from parent
return `${this.name} says: ${this.makeSound()}!`;
}

public getInfo(): string {
// ✅ Can access protected name
// ❌ Can't access private age from parent
return `${this.name} is a ${this.breed}`;
}
}

let dog = new Dog("Buddy", 5, "Golden Retriever");
console.log(dog.bark()); // ✅ Works
console.log(dog.describe()); // ✅ Works

// ❌ Can't access protected members from outside
// console.log(dog.name);
// console.log(dog.makeSound());

When to use protected:

  • Properties that subclasses need to access
  • Methods that subclasses might need to override or use
  • Shared functionality in an inheritance hierarchy

Readonly Properties​

The readonly modifier prevents modification after initialization:

class User {
readonly id: number; // Can only be set in constructor
readonly createdAt: Date; // Can only be set in constructor
public name: string; // Can be modified anytime

constructor(id: number, name: string) {
this.id = id;
this.createdAt = new Date();
this.name = name;
}

updateName(newName: string): void {
this.name = newName; // ✅ Works - not readonly
}
}

let user = new User(1, "Alice");
console.log(user.id); // ✅ Can read
console.log(user.createdAt); // ✅ Can read
user.updateName("Bob"); // ✅ Can update name

// ❌ Errors - readonly properties
// user.id = 2;
// user.createdAt = new Date();

Parameter Properties (Shorthand)​

TypeScript provides a shortcut for declaring and initializing properties in the constructor:

Verbose way:

class Person {
public name: string;
private age: number;

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

Shorthand way:

class Person {
constructor(
public name: string, // Automatically creates and assigns
private age: number // Automatically creates and assigns
) {}
}

Both create the same class!


Examples​

Bank Account System​

class BankAccount {
private balance: number;
public readonly accountNumber: string;
private transactions: string[] = [];

constructor(accountNumber: string, initialBalance: number) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
this.logTransaction("Account opened", initialBalance);
}

public deposit(amount: number): boolean {
if (amount <= 0) {
console.log("Deposit amount must be positive");
return false;
}
this.balance += amount;
this.logTransaction("Deposit", amount);
console.log(`Deposited $${amount}. New balance: $${this.balance}`);
return true;
}

public withdraw(amount: number): boolean {
if (amount <= 0) {
console.log("Withdrawal amount must be positive");
return false;
}
if (amount > this.balance) {
console.log("Insufficient funds");
return false;
}
this.balance -= amount;
this.logTransaction("Withdrawal", amount);
console.log(`Withdrew $${amount}. New balance: $${this.balance}`);
return true;
}

public getBalance(): number {
return this.balance;
}

private logTransaction(type: string, amount: number): void {
const timestamp = new Date().toISOString();
this.transactions.push(`[${timestamp}] ${type}: $${amount}`);
}

public getTransactionHistory(): string[] {
return [...this.transactions]; // Return copy
}
}

// Usage
let account = new BankAccount("ACC001", 1000);
account.deposit(500); // Deposited $500. New balance: $1500
account.withdraw(200); // Withdrew $200. New balance: $1300
account.withdraw(2000); // Insufficient funds
console.log(account.getBalance()); // 1300

Challenges​

1. Create Person with private age​

  • Make age private
  • Add getter for age
  • Add method to celebrate birthday
💡 Show Answer
class Person {
private age: number;

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

getAge(): number {
return this.age;
}

celebrateBirthday(): void {
this.age++;
console.log(`Happy Birthday ${this.name}! You are now ${this.age}`);
}
}

let person = new Person("Alice", 25);
console.log(person.getAge()); // 25
person.celebrateBirthday(); // Happy Birthday Alice! You are now 26


2. BankAccount with transaction limit​

  • Private balance
  • Private daily transaction count
  • Limit to 3 transactions per day
💡 Show Answer
class BankAccount {
private balance: number;
private transactionCount: number = 0;
private lastResetDate: Date;
private readonly maxTransactionsPerDay = 3;

constructor(initialBalance: number) {
this.balance = initialBalance;
this.lastResetDate = new Date();
}

private resetIfNewDay(): void {
const today = new Date().toDateString();
const lastReset = this.lastResetDate.toDateString();

if (today !== lastReset) {
this.transactionCount = 0;
this.lastResetDate = new Date();
}
}

private canTransact(): boolean {
this.resetIfNewDay();
return this.transactionCount < this.maxTransactionsPerDay;
}

public deposit(amount: number): boolean {
if (!this.canTransact()) {
console.log("Daily transaction limit reached");
return false;
}
this.balance += amount;
this.transactionCount++;
return true;
}

public getBalance(): number {
return this.balance;
}
}

let account = new BankAccount(1000);
account.deposit(100);
account.deposit(200);
account.deposit(300);
account.deposit(400); // Daily transaction limit reached

3. Car with protected mileage​

  • Protected mileage (accessible in subclasses)
  • Private maintenance log
  • Public drive method
💡 Show Answer
class Vehicle {
protected mileage: number = 0;

drive(distance: number): void {
if (distance < 0) {
throw new Error("Distance cannot be negative");
}
this.mileage += distance;
}

getMileage(): number {
return this.mileage;
}
}

class Car extends Vehicle {
private maintenanceLog: string[] = [];

drive(distance: number): void {
super.drive(distance);

// Check if maintenance needed (every 5000 km)
if (this.mileage % 5000 < distance) {
this.scheduleMaintenance();
}
}

private scheduleMaintenance(): void {
const log = `Maintenance due at ${this.mileage} km`;
this.maintenanceLog.push(log);
console.log(log);
}

getMaintenanceHistory(): string[] {
return [...this.maintenanceLog];
}
}

let car = new Car();
car.drive(4800);
car.drive(300); // Maintenance due at 5100 km


Summary​

In this chapter, you learned:

✅ Access modifiers: public, private, protected, readonly

✅ How encapsulation protects internal state

✅ Parameter properties shorthand

✅ Getters and setters for controlled access

✅ When to use each access level

✅ Best practices for class design

Key Takeaways:

  • Make fields private by default
  • Expose only what's necessary
  • Use getters/setters for validation
  • readonly prevents accidental modification
  • Parameter properties reduce boilerplate
  • Protected allows subclass access