Access modifiers control the visibility and accessibility of class members (properties and methods). TypeScript provides three access modifiers: public (accessible everywhere), private (accessible only within the class), and protected (accessible within the class and subclasses). These modifiers are essential for encapsulation and data hiding. Let's master access control!
Public Modifier
public members are accessible from anywhere - inside the class, outside the class, and in subclasses. This is the default if no modifier is specified.
class User {
// Explicitly public
public name: string;
public email: string;
// No modifier = public by default
age: number;
constructor(name: string, email: string, age: number) {
this.name = name;
this.email = email;
this.age = age;
}
// Public method
public displayInfo(): void {
console.log(`Name: ${this.name}`);
console.log(`Email: ${this.email}`);
console.log(`Age: ${this.age}`);
}
}
const user = new User("Yusuf Ibrahim", "yusuf@example.com", 25);
// ✅ Can access public properties
console.log(user.name); // "Yusuf Ibrahim"
console.log(user.email); // "yusuf@example.com"
console.log(user.age); // 25
// ✅ Can modify public properties
user.name = "Yusuf A. Ibrahim";
console.log(user.name); // "Yusuf A. Ibrahim"
// ✅ Can call public methods
user.displayInfo();
// Parameter properties with public
class Product {
constructor(
public id: number,
public name: string,
public price: number
) {}
public getFormattedPrice(): string {
return `₦${this.price.toLocaleString()}`;
}
}
const laptop = new Product(1, "Laptop", 450000);
console.log(laptop.name); // "Laptop"
console.log(laptop.price); // 450000
console.log(laptop.getFormattedPrice()); // "₦450,000"💡 Public is the Default
If you don't specify an access modifier, members are public by default. However, being explicit with public improves code readability and makes your intentions clear.
Private Modifier
private members are only accessible within the class where they're defined. They cannot be accessed from outside the class or from subclasses.
class BankAccount {
public accountNumber: string;
private balance: number; // Only accessible inside this class
private pin: string;
constructor(accountNumber: string, initialBalance: number, pin: string) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
this.pin = pin;
}
// Public method to access private data
public getBalance(): number {
return this.balance;
}
// Public method with validation
public deposit(amount: number): void {
if (amount <= 0) {
throw new Error("Deposit amount must be positive");
}
this.balance += amount;
console.log(`Deposited ₦${amount}. New balance: ₦${this.balance}`);
}
// Public method with PIN verification
public withdraw(amount: number, inputPin: string): boolean {
if (!this.verifyPin(inputPin)) {
console.log("Invalid PIN");
return false;
}
if (amount > this.balance) {
console.log("Insufficient funds");
return false;
}
this.balance -= amount;
console.log(`Withdrew ₦${amount}. New balance: ₦${this.balance}`);
return true;
}
// Private helper method
private verifyPin(inputPin: string): boolean {
return this.pin === inputPin;
}
// Private method to calculate interest
private calculateInterest(rate: number): number {
return this.balance * rate;
}
public applyInterest(rate: number): void {
const interest = this.calculateInterest(rate);
this.balance += interest;
console.log(`Applied ₦${interest} interest. New balance: ₦${this.balance}`);
}
}
const account = new BankAccount("0123456789", 10000, "1234");
// ✅ Can access public property
console.log(account.accountNumber); // "0123456789"
// ❌ Cannot access private properties directly
// console.log(account.balance); // Error: Property 'balance' is private
// console.log(account.pin); // Error: Property 'pin' is private
// ✅ Access private data through public methods
console.log(account.getBalance()); // 10000
account.deposit(5000); // "Deposited ₦5000. New balance: ₦15000"
account.withdraw(3000, "1234"); // "Withdrew ₦3000. New balance: ₦12000"
account.withdraw(1000, "wrong"); // "Invalid PIN"
account.applyInterest(0.05); // "Applied ₦600 interest. New balance: ₦12600"
// ❌ Cannot call private methods
// account.verifyPin("1234"); // Error: Method 'verifyPin' is privatePrivate Parameter Properties
class User {
// Private parameter properties
constructor(
public username: string,
private password: string, // Private from the start
private apiKey: string
) {}
public authenticate(inputPassword: string): boolean {
return this.password === inputPassword;
}
public changePassword(oldPassword: string, newPassword: string): boolean {
if (this.password !== oldPassword) {
return false;
}
if (newPassword.length < 8) {
throw new Error("Password must be at least 8 characters");
}
this.password = newPassword;
return true;
}
// API key is completely private - no public access
private getApiKey(): string {
return this.apiKey;
}
}
const user = new User("yusuf", "secret123", "api-key-xyz");
console.log(user.username); // ✅ "yusuf" (public)
// console.log(user.password); // ❌ Error: private
// console.log(user.apiKey); // ❌ Error: private
console.log(user.authenticate("secret123")); // true
user.changePassword("secret123", "newpass456");Protected Modifier
protected members are accessible within the class and its subclasses, but not from outside instances.
class Employee {
public name: string;
protected salary: number; // Accessible in subclasses
private ssn: string; // Only in this class
constructor(name: string, salary: number, ssn: string) {
this.name = name;
this.salary = salary;
this.ssn = ssn;
}
public displayPublicInfo(): void {
console.log(`Name: ${this.name}`);
}
protected displayProtectedInfo(): void {
console.log(`Salary: ₦${this.salary}`);
}
private displayPrivateInfo(): void {
console.log(`SSN: ${this.ssn}`);
}
public displayAllInfo(): void {
this.displayPublicInfo();
this.displayProtectedInfo();
this.displayPrivateInfo();
}
}
class Manager extends Employee {
constructor(
name: string,
salary: number,
ssn: string,
public department: string
) {
super(name, salary, ssn);
}
public giveBonusPercentage(percentage: number): void {
// ✅ Can access protected salary in subclass
const bonus = this.salary * (percentage / 100);
this.salary += bonus;
console.log(`Added ₦${bonus} bonus. New salary: ₦${this.salary}`);
}
public displayManagerInfo(): void {
console.log(`Name: ${this.name}`); // ✅ public
// ✅ Can call protected method in subclass
this.displayProtectedInfo();
console.log(`Department: ${this.department}`);
// ❌ Cannot access private members
// console.log(`SSN: ${this.ssn}`); // Error: private
// this.displayPrivateInfo(); // Error: private method
}
}
const employee = new Employee("Yusuf", 500000, "123-45-6789");
const manager = new Manager("Amina", 800000, "987-65-4321", "Engineering");
// ✅ Can access public members
console.log(employee.name); // "Yusuf"
console.log(manager.name); // "Amina"
console.log(manager.department); // "Engineering"
// ❌ Cannot access protected members from outside
// console.log(employee.salary); // Error: protected
// console.log(manager.salary); // Error: protected
// ❌ Cannot access private members from outside
// console.log(employee.ssn); // Error: private
// ❌ Cannot call protected methods from outside
// employee.displayProtectedInfo(); // Error: protected method
// ✅ Subclass can use protected members internally
manager.giveBonusPercentage(10);
// "Added ₦80000 bonus. New salary: ₦880000"
manager.displayManagerInfo();
// Name: Amina
// Salary: ₦880000
// Department: EngineeringProtected Constructors
// Protected constructor prevents direct instantiation
class Database {
protected constructor(private connectionString: string) {
console.log(`Connected to: ${connectionString}`);
}
protected query(sql: string): void {
console.log(`Executing: ${sql}`);
}
}
// ❌ Cannot instantiate directly
// const db = new Database("localhost"); // Error: constructor is protected
// ✅ Can extend and use in subclass
class MySQLDatabase extends Database {
constructor(host: string, database: string) {
super(`mysql://${host}/${database}`);
}
public executeQuery(sql: string): void {
// Can call protected method
this.query(sql);
}
}
const mysql = new MySQLDatabase("localhost", "myapp");
mysql.executeQuery("SELECT * FROM users");
// Singleton pattern with protected constructor
class Logger {
private static instance: Logger;
protected constructor() {
console.log("Logger initialized");
}
public static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
public log(message: string): void {
console.log(`[LOG] ${message}`);
}
}
// ❌ Cannot create directly
// const logger1 = new Logger(); // Error: protected constructor
// ✅ Get singleton instance
const logger = Logger.getInstance();
logger.log("Application started");
const sameLogger = Logger.getInstance();
console.log(logger === sameLogger); // true (same instance)Readonly Modifier
readonly properties can only be assigned during initialization (declaration or constructor). They cannot be modified afterward.
class User {
readonly id: number; // Can only set in constructor
readonly createdAt: Date = new Date(); // Or at declaration
public name: string;
constructor(id: number, name: string) {
this.id = id;
this.name = name;
// Can modify readonly in constructor
this.createdAt = new Date();
}
public updateName(newName: string): void {
this.name = newName; // ✅ OK - not readonly
// this.id = 999; // ❌ Error: readonly
}
}
const user = new User(1, "Yusuf");
console.log(user.id); // 1
console.log(user.createdAt); // Date
user.name = "Yusuf Ibrahim"; // ✅ OK
// user.id = 2; // ❌ Error: readonly property
// user.createdAt = new Date(); // ❌ Error: readonly property
// Readonly with access modifiers
class Product {
constructor(
public readonly id: number, // Public and readonly
private readonly sku: string, // Private and readonly
public name: string
) {}
public displayInfo(): void {
console.log(`ID: ${this.id}`);
console.log(`SKU: ${this.sku}`); // Can read private readonly
console.log(`Name: ${this.name}`);
}
}
const product = new Product(101, "LAP-001", "Laptop");
console.log(product.id); // ✅ 101 (public readonly)
// console.log(product.sku); // ❌ Error: private
product.name = "Gaming Laptop"; // ✅ OK (not readonly)
// product.id = 102; // ❌ Error: readonly
// Readonly arrays and objects
class Team {
readonly members: readonly string[]; // Readonly array
constructor(members: string[]) {
this.members = members;
}
}
const team = new Team(["Yusuf", "Amina", "Chidi"]);
console.log(team.members[0]); // ✅ "Yusuf"
// team.members.push("David"); // ❌ Error: readonly array
// team.members[0] = "John"; // ❌ Error: readonly arrayReadonly vs Const: Use readonly for class properties and const for variables. readonly can be set in the constructor, while const must be initialized at declaration.
Combining Access Modifiers
class Order {
// Different combinations
public readonly orderId: number; // Public, immutable
private readonly createdAt: Date; // Private, immutable
protected readonly customerId: number; // Protected, immutable
private status: string; // Private, mutable
public items: string[]; // Public, mutable
constructor(orderId: number, customerId: number) {
this.orderId = orderId;
this.customerId = customerId;
this.createdAt = new Date();
this.status = "pending";
this.items = [];
}
// Public method accessing private data
public getStatus(): string {
return this.status;
}
public addItem(item: string): void {
if (this.status === "shipped") {
throw new Error("Cannot modify shipped order");
}
this.items.push(item);
}
// Protected method for subclasses
protected updateStatus(newStatus: string): void {
this.status = newStatus;
console.log(`Order ${this.orderId} status: ${newStatus}`);
}
// Private helper method
private validateStatusTransition(newStatus: string): boolean {
const validTransitions: Record<string, string[]> = {
pending: ["processing", "cancelled"],
processing: ["shipped", "cancelled"],
shipped: ["delivered"],
delivered: [],
cancelled: []
};
return validTransitions[this.status]?.includes(newStatus) ?? false;
}
}
class AdminOrder extends Order {
constructor(orderId: number, customerId: number) {
super(orderId, customerId);
}
// Can access protected members
public forceUpdateStatus(newStatus: string): void {
console.log(`Admin override for customer ${this.customerId}`);
this.updateStatus(newStatus); // ✅ Protected method
}
// Cannot access private members
public getCreationTime(): void {
// console.log(this.createdAt); // ❌ Error: private
// console.log(this.status); // ❌ Error: private
}
}
const order = new Order(1001, 50);
// Public members
console.log(order.orderId); // ✅ 1001
console.log(order.items); // ✅ []
order.addItem("Laptop"); // ✅ Works
// Readonly public member
// order.orderId = 1002; // ❌ Error: readonly
// Protected and private members
// console.log(order.customerId); // ❌ Error: protected
// console.log(order.status); // ❌ Error: private
// order.updateStatus("shipped"); // ❌ Error: protected method
// Access through public methods
console.log(order.getStatus()); // "pending"
const adminOrder = new AdminOrder(1002, 51);
adminOrder.forceUpdateStatus("delivered"); // Works in subclassPractical Examples
Example 1: Secure Authentication System
class AuthenticationService {
private readonly users: Map<string, string> = new Map();
private readonly sessions: Map<string, { username: string; expiresAt: Date }> = new Map();
private readonly sessionDuration: number = 3600000; // 1 hour
public register(username: string, password: string): boolean {
if (this.users.has(username)) {
console.log("Username already exists");
return false;
}
if (!this.validatePassword(password)) {
console.log("Password does not meet requirements");
return false;
}
const hashedPassword = this.hashPassword(password);
this.users.set(username, hashedPassword);
console.log(`User ${username} registered successfully`);
return true;
}
public login(username: string, password: string): string | null {
const storedHash = this.users.get(username);
if (!storedHash) {
console.log("User not found");
return null;
}
if (this.hashPassword(password) !== storedHash) {
console.log("Invalid password");
return null;
}
const sessionId = this.generateSessionId();
const expiresAt = new Date(Date.now() + this.sessionDuration);
this.sessions.set(sessionId, { username, expiresAt });
console.log(`User ${username} logged in`);
return sessionId;
}
public logout(sessionId: string): boolean {
const session = this.sessions.get(sessionId);
if (!session) {
return false;
}
this.sessions.delete(sessionId);
console.log(`User ${session.username} logged out`);
return true;
}
public validateSession(sessionId: string): boolean {
const session = this.sessions.get(sessionId);
if (!session) {
return false;
}
if (new Date() > session.expiresAt) {
this.sessions.delete(sessionId);
return false;
}
return true;
}
// Private helper methods
private validatePassword(password: string): boolean {
return password.length >= 8 &&
/[A-Z]/.test(password) &&
/[a-z]/.test(password) &&
/[0-9]/.test(password);
}
private hashPassword(password: string): string {
// Simple hash for demo (use proper hashing in production)
return `hashed_${password}`;
}
private generateSessionId(): string {
return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
}
// Usage
const auth = new AuthenticationService();
// Register users
auth.register("yusuf", "SecurePass123");
auth.register("amina", "StrongPwd456");
// Login
const session1 = auth.login("yusuf", "SecurePass123");
if (session1) {
console.log(`Session ID: ${session1}`);
// Validate session
console.log(auth.validateSession(session1)); // true
// Logout
auth.logout(session1);
console.log(auth.validateSession(session1)); // false
}
// Cannot access private data
// console.log(auth.users); // ❌ Error: private
// console.log(auth.hashPassword("test")); // ❌ Error: private methodExample 2: Bank Account with Transaction History
type TransactionType = "deposit" | "withdrawal" | "transfer";
interface Transaction {
readonly id: number;
readonly type: TransactionType;
readonly amount: number;
readonly date: Date;
readonly description: string;
}
class BankAccount {
public readonly accountNumber: string;
public readonly accountHolder: string;
private balance: number;
private readonly transactions: Transaction[] = [];
private transactionIdCounter: number = 1;
constructor(accountNumber: string, accountHolder: string, initialBalance: number = 0) {
this.accountNumber = accountNumber;
this.accountHolder = accountHolder;
this.balance = initialBalance;
if (initialBalance > 0) {
this.recordTransaction("deposit", initialBalance, "Initial deposit");
}
}
public deposit(amount: number): void {
this.validateAmount(amount);
this.balance += amount;
this.recordTransaction("deposit", amount, "Deposit");
console.log(`Deposited ₦${amount}. Balance: ₦${this.balance}`);
}
public withdraw(amount: number): boolean {
this.validateAmount(amount);
if (amount > this.balance) {
console.log("Insufficient funds");
return false;
}
this.balance -= amount;
this.recordTransaction("withdrawal", amount, "Withdrawal");
console.log(`Withdrew ₦${amount}. Balance: ₦${this.balance}`);
return true;
}
public transfer(amount: number, recipient: BankAccount): boolean {
if (!this.withdraw(amount)) {
return false;
}
recipient.deposit(amount);
this.updateLastTransaction(`Transfer to ${recipient.accountHolder}`);
console.log(`Transferred ₦${amount} to ${recipient.accountHolder}`);
return true;
}
public getBalance(): number {
return this.balance;
}
public getTransactionHistory(): readonly Transaction[] {
return [...this.transactions]; // Return copy
}
public getStatement(count: number = 10): void {
console.log(`\n=== Statement for ${this.accountHolder} ===`);
console.log(`Account: ${this.accountNumber}`);
console.log(`Balance: ₦${this.balance}\n`);
const recent = this.transactions.slice(-count).reverse();
recent.forEach(transaction => {
const sign = transaction.type === "deposit" ? "+" : "-";
console.log(
`[${transaction.date.toLocaleDateString()}] ` +
`${transaction.description}: ${sign}₦${transaction.amount}`
);
});
console.log("=".repeat(50) + "\n");
}
// Private helper methods
private validateAmount(amount: number): void {
if (amount <= 0) {
throw new Error("Amount must be positive");
}
}
private recordTransaction(type: TransactionType, amount: number, description: string): void {
this.transactions.push({
id: this.transactionIdCounter++,
type,
amount,
date: new Date(),
description
});
}
private updateLastTransaction(description: string): void {
const last = this.transactions[this.transactions.length - 1];
if (last) {
(last as any).description = description; // Modify description
}
}
}
// Usage
const account1 = new BankAccount("0123456789", "Yusuf Ibrahim", 50000);
const account2 = new BankAccount("9876543210", "Amina Hassan", 30000);
account1.deposit(20000);
account1.withdraw(10000);
account1.transfer(5000, account2);
account1.getStatement();
// === Statement for Yusuf Ibrahim ===
// Account: 0123456789
// Balance: ₦55000
//
// [12/26/2025] Transfer to Amina Hassan: -₦5000
// [12/26/2025] Withdrawal: -₦10000
// [12/26/2025] Deposit: +₦20000
// [12/26/2025] Initial deposit: +₦50000
console.log(account2.getBalance()); // 35000
// Cannot access private data
// console.log(account1.balance); // ❌ Error: private
// console.log(account1.transactions); // ❌ Error: private
// Readonly property
console.log(account1.accountNumber); // ✅ "0123456789"
// account1.accountNumber = "111"; // ❌ Error: readonlyTest Your Knowledge
Type Safety Check
Which access modifier usage is correct?
class BankAccount {
// Option A
public accountNumber: string;
private balance: number;
protected customerName: string;
constructor(accountNumber: string, customerName: string) {
this.accountNumber = accountNumber;
this.balance = 0;
this.customerName = customerName;
}
public deposit(amount: number): void {
this.balance += amount;
}
public getBalance(): number {
return this.balance;
}
}
const account = new BankAccount("0123456789", "Yusuf");
// Which is valid?
// Option B
console.log(account.accountNumber); // Access public property
// Option C
console.log(account.balance); // Access private property
// Option D
console.log(account.customerName); // Access protected propertyCommon TypeScript Error
class User {
private password: string;
protected email: string;
constructor(email: string, password: string) {
this.email = email;
this.password = password;
}
}
const user = new User("yusuf@example.com", "secret123");
// Trying to access private property
console.log(user.password); // ❌ Error!
// Trying to access protected property
console.log(user.email); // ❌ Error!
// Trying to modify private property
user.password = "newpassword"; // ❌ Error!❌ Property 'password' is private and only accessible within class 'User'. Property 'email' is protected and only accessible within class 'User' and its subclasses.
What's Wrong?
Private properties can only be accessed inside the class. Protected properties can be accessed in the class and subclasses, but not from outside instances. This is encapsulation in action!
class User {
private password: string;
protected email: string;
constructor(email: string, password: string) {
this.email = email;
this.password = password;
}
// Public method to verify password
public verifyPassword(inputPassword: string): boolean {
return this.password === inputPassword;
}
// Public method to update password
public updatePassword(oldPassword: string, newPassword: string): boolean {
if (this.password !== oldPassword) {
return false;
}
if (newPassword.length < 8) {
throw new Error("Password must be at least 8 characters");
}
this.password = newPassword;
return true;
}
// Public method to get email
public getEmail(): string {
return this.email;
}
}
const user = new User("yusuf@example.com", "secret123");
// ✅ Access through public methods
console.log(user.verifyPassword("secret123")); // true
console.log(user.verifyPassword("wrong")); // false
user.updatePassword("secret123", "newsecret456");
console.log(user.verifyPassword("newsecret456")); // true
console.log(user.getEmail()); // "yusuf@example.com"Solution: Provide public methods to access or modify private/protected data
Best Practices
- Default to private - only make public what needs to be
- Use protected for inheritance - share with subclasses only
- Make IDs and timestamps readonly - prevent accidental changes
- Provide public methods for private data - controlled access
- Validate in public methods - protect internal state
- Use private for helper methods - hide implementation details
- Be explicit with modifiers - improve readability
- Combine readonly with private/protected - maximum protection
// ✅ Good: Proper encapsulation
class Product {
public readonly id: number; // Public, immutable
private price: number; // Private, controlled access
protected stock: number; // For subclasses
constructor(id: number, price: number, stock: number) {
this.id = id;
this.price = this.validatePrice(price);
this.stock = stock;
}
// Public getter with validation
public getPrice(): number {
return this.price;
}
// Public setter with validation
public updatePrice(newPrice: number): void {
this.price = this.validatePrice(newPrice);
}
// Private validation
private validatePrice(price: number): number {
if (price < 0) {
throw new Error("Price cannot be negative");
}
return price;
}
}
// ❌ Avoid: Everything public
class BadProduct {
public id: number; // Should be readonly
public price: number; // Should be private
public stock: number; // Should be protected/private
constructor(id: number, price: number, stock: number) {
this.id = id;
this.price = price;
this.stock = stock;
// No validation!
}
}
// ❌ Avoid: Everything private
class TooPrivate {
private name: string;
private email: string;
constructor(name: string, email: string) {
this.name = name;
this.email = email;
}
// No way to access data!
}Key Takeaways
publicmembers are accessible everywhere (default)privatemembers are only accessible within the classprotectedmembers are accessible in class and subclassesreadonlyproperties can only be set during initialization- Access modifiers enable encapsulation and data hiding
- Use private for internal implementation details
- Use protected to share with subclasses
- Combine modifiers for fine-grained control
- Provide public methods to access private/protected data
- Default to private, make public only when necessary
What's Next?
Excellent work! You've mastered access modifiers and understand how to properly encapsulate your classes. Next, we'll explore Inheritance and Polymorphism—how to extend classes, reuse code, and implement polymorphic behavior using the extends keyword and method overriding. These are fundamental OOP concepts!
Get ready to master inheritance and build powerful class hierarchies!