Prerequisites
- Understanding of TypeScript classes and constructors ποΈ
- Basic knowledge of object-oriented programming π
- Familiarity with inheritance concepts π₯
What you'll learn
- Master all four access modifiers and their use cases π―
- Implement proper encapsulation in TypeScript classes π
- Design secure inheritance hierarchies π‘οΈ
- Create maintainable and robust class interfaces β¨
π― Introduction
Welcome to the fortress of TypeScript access modifiers! π° Think of access modifiers as security guards for your class members - they control who can access what, when, and how.
Just like a house has different levels of access (public lobby, private bedroom, family room for relatives), your classes need different levels of member visibility. Access modifiers are your tools for creating secure, well-designed code that follows the principle of βleast privilegeβ π.
By the end of this tutorial, youβll be the master of encapsulation, crafting classes that are both powerful and secure! Letβs unlock the secrets of access control! ποΈ
π Understanding Access Modifiers
π€ What are Access Modifiers?
Access modifiers control the visibility and accessibility of class members:
- public π: Accessible from anywhere
- private π: Only accessible within the same class
- protected π₯: Accessible within the class and subclasses
- readonly π: Can be assigned once, then becomes immutable
Think of them like security clearance levels:
- π Public: Anyone can enter (lobby)
- π₯ Protected: Family members only (family room)
- π Private: Owner only (private office)
- π Readonly: Look but donβt touch (museum display)
π‘ Why Use Access Modifiers?
Hereβs why access modifiers are essential:
- Encapsulation π: Hide internal implementation details
- Security π‘οΈ: Prevent unauthorized access to sensitive data
- Maintainability π οΈ: Control how your class can be used
- API Design π―: Create clean, intuitive interfaces
- Inheritance Control π₯: Manage what subclasses can access
π Public Access Modifier
β Public - The Open Door
public
members are accessible from anywhere - inside the class, outside the class, and in subclasses.
// π€ User class with public members
class User {
// π Public properties - accessible everywhere
public name: string;
public email: string;
public isActive: boolean;
constructor(name: string, email: string) {
this.name = name;
this.email = email;
this.isActive = true;
console.log(`π Welcome ${name}!`);
}
// π Public methods - can be called from anywhere
public login(): void {
if (this.isActive) {
console.log(`π ${this.name} logged in successfully!`);
} else {
console.log(`β Account for ${this.name} is inactive`);
}
}
public logout(): void {
console.log(`π ${this.name} logged out`);
}
// π Public getter/setter
public getProfile(): string {
return `π€ ${this.name} (${this.email}) - ${this.isActive ? 'β
Active' : 'β Inactive'}`;
}
public updateEmail(newEmail: string): void {
// π‘οΈ Even public methods can have validation
if (newEmail.includes('@')) {
this.email = newEmail;
console.log(`π§ Email updated to: ${newEmail}`);
} else {
console.log(`β Invalid email format: ${newEmail}`);
}
}
}
// π Public members are accessible everywhere
const user = new User("Alice", "[email protected]");
// β
Direct access to public properties
console.log(user.name); // "Alice"
console.log(user.email); // "[email protected]"
user.isActive = false; // Direct modification allowed
// β
Direct access to public methods
user.login(); // Works from outside
user.updateEmail("[email protected]");
console.log(user.getProfile());
// π‘ Note: Default visibility in TypeScript is 'public'
class SimpleClass {
// These are implicitly public
property: string = "I'm public!";
method(): void {
console.log("I'm also public!");
}
}
π Private Access Modifier
π‘οΈ Private - Fort Knox Security
private
members are only accessible within the same class - not from outside or even from subclasses.
// π¦ BankAccount class with private members
class BankAccount {
// π Public interface
public readonly accountNumber: string;
public readonly accountHolder: string;
// π Private properties - internal use only
private balance: number;
private pin: string;
private transactionHistory: string[];
private isLocked: boolean;
constructor(accountHolder: string, initialDeposit: number, pin: string) {
this.accountNumber = this.generateAccountNumber();
this.accountHolder = accountHolder;
this.balance = initialDeposit;
this.pin = pin;
this.transactionHistory = [`Initial deposit: $${initialDeposit}`];
this.isLocked = false;
console.log(`π¦ Account ${this.accountNumber} created for ${accountHolder}`);
}
// π Private method - only used internally
private generateAccountNumber(): string {
return `ACC${Date.now()}${Math.floor(Math.random() * 1000)}`;
}
// π Private validation method
private validatePin(inputPin: string): boolean {
return this.pin === inputPin;
}
// π Private method to add transaction
private addTransaction(description: string): void {
const timestamp = new Date().toLocaleString();
this.transactionHistory.push(`${timestamp}: ${description}`);
}
// π Public methods that use private members safely
public checkBalance(pin: string): number | null {
if (this.isLocked) {
console.log("π Account is locked. Contact support.");
return null;
}
if (!this.validatePin(pin)) {
console.log("β Invalid PIN");
return null;
}
console.log(`π° Balance: $${this.balance.toFixed(2)}`);
return this.balance;
}
public deposit(amount: number, pin: string): boolean {
if (this.isLocked || !this.validatePin(pin)) {
console.log("β Transaction denied");
return false;
}
if (amount <= 0) {
console.log("β Deposit amount must be positive");
return false;
}
this.balance += amount;
this.addTransaction(`Deposit: +$${amount.toFixed(2)}`);
console.log(`β
Deposited $${amount.toFixed(2)}. New balance: $${this.balance.toFixed(2)}`);
return true;
}
public withdraw(amount: number, pin: string): boolean {
if (this.isLocked || !this.validatePin(pin)) {
console.log("β Transaction denied");
return false;
}
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.addTransaction(`Withdrawal: -$${amount.toFixed(2)}`);
console.log(`β
Withdrew $${amount.toFixed(2)}. Remaining balance: $${this.balance.toFixed(2)}`);
return true;
}
// π Private method for internal use
private lockAccount(): void {
this.isLocked = true;
this.addTransaction("Account locked due to security concerns");
console.log("π Account has been locked for security");
}
// π Public method to get transaction history (with PIN)
public getTransactionHistory(pin: string): string[] | null {
if (!this.validatePin(pin)) {
console.log("β Invalid PIN");
return null;
}
return [...this.transactionHistory]; // Return copy to prevent modification
}
}
// π Test private access control
const account = new BankAccount("John Doe", 1000, "1234");
// β
These work - public interface
console.log(account.accountNumber);
console.log(account.accountHolder);
account.checkBalance("1234");
account.deposit(500, "1234");
// β These would cause TypeScript errors:
// console.log(account.balance); // Error: 'balance' is private
// account.pin = "5678"; // Error: 'pin' is private
// account.validatePin("1234"); // Error: 'validatePin' is private
// account.lockAccount(); // Error: 'lockAccount' is private
console.log("π³ Account Details:");
console.log(`Account: ${account.accountNumber}`);
console.log(`Holder: ${account.accountHolder}`);
π₯ Protected Access Modifier
π Protected - Family Only
protected
members are accessible within the class and its subclasses, but not from outside.
// πΎ Animal base class with protected members
class Animal {
// π Public interface
public name: string;
public species: string;
// π₯ Protected - available to subclasses
protected age: number;
protected energy: number;
protected habitat: string;
// π Private - only for this class
private id: string;
constructor(name: string, species: string, age: number, habitat: string) {
this.name = name;
this.species = species;
this.age = age;
this.energy = 100;
this.habitat = habitat;
this.id = `${species}_${Date.now()}`;
console.log(`πΎ ${species} ${name} has been born!`);
}
// π₯ Protected methods - subclasses can use these
protected consumeEnergy(amount: number): void {
this.energy = Math.max(0, this.energy - amount);
console.log(`β‘ ${this.name} consumed ${amount} energy. Remaining: ${this.energy}`);
}
protected restoreEnergy(amount: number): void {
this.energy = Math.min(100, this.energy + amount);
console.log(`π΄ ${this.name} restored ${amount} energy. Current: ${this.energy}`);
}
protected checkHabitat(requiredHabitat: string): boolean {
return this.habitat === requiredHabitat;
}
// π Private method - only this class can use
private generateId(): string {
return `${this.species}_${Date.now()}`;
}
// π Public methods
public getInfo(): string {
return `πΎ ${this.name} the ${this.species} (${this.age} years old) - Energy: ${this.energy}%`;
}
public sleep(): void {
console.log(`π΄ ${this.name} is sleeping...`);
this.restoreEnergy(30);
}
}
// π Dog class extending Animal
class Dog extends Animal {
private tricks: string[];
constructor(name: string, age: number, breed: string) {
super(name, "Dog", age, "Domestic"); // Call parent constructor
this.tricks = [];
console.log(`π ${breed} dog ${name} is ready to play!`);
}
// π Dog-specific method using protected members
public bark(): void {
// β
Can access protected members from parent
if (this.energy < 10) {
console.log(`π΄ ${this.name} is too tired to bark`);
return;
}
console.log(`π ${this.name} says: Woof! Woof!`);
this.consumeEnergy(5); // β
Using protected method
}
public play(): void {
// β
Can check protected habitat
if (!this.checkHabitat("Domestic")) {
console.log(`π ${this.name} needs to be in a domestic environment to play safely`);
return;
}
console.log(`πΎ ${this.name} is playing fetch!`);
this.consumeEnergy(15); // β
Using protected method
}
public learnTrick(trick: string): void {
if (this.energy < 20) {
console.log(`π΄ ${this.name} is too tired to learn new tricks`);
return;
}
this.tricks.push(trick);
this.consumeEnergy(10);
console.log(`πͺ ${this.name} learned to ${trick}!`);
}
// π― Override parent method with additional functionality
public getInfo(): string {
const baseInfo = super.getInfo(); // β
Call parent method
const tricksInfo = this.tricks.length > 0 ? ` | Tricks: ${this.tricks.join(', ')}` : '';
return `${baseInfo}${tricksInfo}`;
}
// π Method that accesses protected properties
public getDetailedStatus(): string {
// β
Can access protected properties
return `
π ${this.name} Status Report:
π Age: ${this.age} years
β‘ Energy: ${this.energy}%
π Habitat: ${this.habitat}
πͺ Tricks Known: ${this.tricks.length}
`.trim();
}
}
// π± Cat class extending Animal
class Cat extends Animal {
private lives: number = 9;
constructor(name: string, age: number, isIndoor: boolean) {
const habitat = isIndoor ? "Indoor" : "Outdoor";
super(name, "Cat", age, habitat);
console.log(`π± ${name} has ${this.lives} lives remaining!`);
}
public meow(): void {
if (this.energy < 5) {
console.log(`π΄ ${this.name} is too tired to meow`);
return;
}
console.log(`π± ${this.name} says: Meow!`);
this.consumeEnergy(3); // β
Using protected method
}
public hunt(): void {
// β
Check protected habitat
if (this.checkHabitat("Indoor")) {
console.log(`π ${this.name} is an indoor cat and can't hunt outside`);
return;
}
if (this.energy < 25) {
console.log(`π΄ ${this.name} is too tired to hunt`);
return;
}
console.log(`π― ${this.name} is hunting prey!`);
this.consumeEnergy(20);
// Sometimes hunting is dangerous!
if (Math.random() < 0.1) {
this.loseLife();
}
}
private loseLife(): void {
if (this.lives > 1) {
this.lives--;
console.log(`π ${this.name} lost a life! ${this.lives} lives remaining`);
} else {
console.log(`πΏ ${this.name} has used all their lives`);
}
}
// π Cat-specific status
public getCatStatus(): string {
// β
Access protected properties
const status = this.checkHabitat("Indoor") ? "π Indoor" : "π³ Outdoor";
return `π± ${this.name}: ${this.age} years, ${this.energy}% energy, ${this.lives} lives, ${status}`;
}
}
// π Test protected access
const buddy = new Dog("Buddy", 3, "Golden Retriever");
const whiskers = new Cat("Whiskers", 2, false);
console.log("\nπ Animal Information:");
console.log(buddy.getInfo());
console.log(whiskers.getInfo());
console.log("\nπͺ Animal Actions:");
buddy.bark();
buddy.play();
buddy.learnTrick("sit");
whiskers.meow();
whiskers.hunt();
console.log("\nπ Detailed Status:");
console.log(buddy.getDetailedStatus());
console.log(whiskers.getCatStatus());
// β
These work - public methods
buddy.sleep();
whiskers.sleep();
// β These would cause TypeScript errors:
// console.log(buddy.age); // Error: 'age' is protected
// buddy.consumeEnergy(10); // Error: 'consumeEnergy' is protected
// console.log(whiskers.energy); // Error: 'energy' is protected
π Readonly Access Modifier
π Readonly - Look But Donβt Touch
readonly
properties can only be assigned during declaration or in the constructor, then become immutable.
// π Document class with readonly properties
class Document {
// π Readonly properties - can only be set once
public readonly id: string;
public readonly createdAt: Date;
public readonly author: string;
public readonly type: "pdf" | "docx" | "txt";
// π Mutable properties
public title: string;
public content: string;
public isPublished: boolean;
// π Readonly array - reference is readonly, but contents can change
public readonly tags: string[];
// π Readonly object - reference is readonly
public readonly metadata: {
version: number;
lastModified: Date;
size: number;
};
constructor(
author: string,
title: string,
type: "pdf" | "docx" | "txt",
content: string = ""
) {
// β
Can assign readonly properties in constructor
this.id = `doc_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
this.createdAt = new Date();
this.author = author;
this.type = type;
// Regular property assignment
this.title = title;
this.content = content;
this.isPublished = false;
// β
Initialize readonly array and object
this.tags = [];
this.metadata = {
version: 1,
lastModified: new Date(),
size: content.length
};
console.log(`π Document "${title}" created by ${author}`);
}
// π Update content (updates metadata)
public updateContent(newContent: string): void {
this.content = newContent;
// β Can't reassign readonly object, but can modify its properties
// this.metadata = { ... }; // Error!
// β
Can modify properties of readonly object
this.metadata.version++;
this.metadata.lastModified = new Date();
this.metadata.size = newContent.length;
console.log(`π Document updated. Version: ${this.metadata.version}`);
}
// π·οΈ Add tag
public addTag(tag: string): void {
// β Can't reassign readonly array
// this.tags = [...this.tags, tag]; // Error!
// β
Can modify contents of readonly array
if (!this.tags.includes(tag)) {
this.tags.push(tag);
console.log(`π·οΈ Added tag: ${tag}`);
}
}
// ποΈ Remove tag
public removeTag(tag: string): void {
const index = this.tags.indexOf(tag);
if (index !== -1) {
this.tags.splice(index, 1);
console.log(`ποΈ Removed tag: ${tag}`);
}
}
// π Document info
public getDocumentInfo(): string {
const publishStatus = this.isPublished ? "π’ Published" : "π Draft";
const tagsStr = this.tags.length > 0 ? this.tags.join(", ") : "No tags";
return `
π ${this.title} (${this.type.toUpperCase()})
π€ Author: ${this.author}
π ID: ${this.id}
π
Created: ${this.createdAt.toLocaleDateString()}
π Status: ${publishStatus}
π Version: ${this.metadata.version}
π Size: ${this.metadata.size} characters
π·οΈ Tags: ${tagsStr}
`.trim();
}
// π’ Publish document
public publish(): void {
if (this.content.trim().length === 0) {
console.log("β Cannot publish empty document");
return;
}
this.isPublished = true;
this.metadata.lastModified = new Date();
console.log(`π’ Document "${this.title}" has been published!`);
}
}
// π¨ Design Document with readonly configuration
class DesignDocument extends Document {
// π Additional readonly properties in subclass
public readonly designTool: string;
public readonly resolution: { width: number; height: number };
constructor(
author: string,
title: string,
designTool: string,
resolution: { width: number; height: number }
) {
super(author, title, "pdf");
// β
Can set readonly properties in subclass constructor
this.designTool = designTool;
this.resolution = resolution; // Object reference is readonly
console.log(`π¨ Design document created with ${designTool}`);
}
// πΌοΈ Update resolution (modify readonly object properties)
public updateResolution(width: number, height: number): void {
// β Can't reassign readonly object
// this.resolution = { width, height }; // Error!
// β
Can modify properties of readonly object
this.resolution.width = width;
this.resolution.height = height;
this.metadata.lastModified = new Date();
console.log(`πΌοΈ Resolution updated to ${width}x${height}`);
}
// π Override to include design info
public getDocumentInfo(): string {
const baseInfo = super.getDocumentInfo();
const designInfo = `\nπ¨ Design Tool: ${this.designTool}\nπΌοΈ Resolution: ${this.resolution.width}x${this.resolution.height}`;
return baseInfo + designInfo;
}
}
// π Test readonly behavior
const report = new Document(
"Alice Johnson",
"TypeScript Best Practices",
"docx",
"This document covers TypeScript best practices..."
);
const logo = new DesignDocument(
"Bob Designer",
"Company Logo",
"Adobe Illustrator",
{ width: 1920, height: 1080 }
);
console.log("\nπ Document Operations:");
// β
These work - mutable properties
report.title = "Advanced TypeScript Best Practices";
report.updateContent("Updated content with more examples...");
report.addTag("typescript");
report.addTag("programming");
report.publish();
// β
Can modify properties of readonly objects
logo.updateResolution(2560, 1440);
console.log("\nπ Document Information:");
console.log(report.getDocumentInfo());
console.log("\n" + logo.getDocumentInfo());
// β These would cause TypeScript errors:
// report.id = "new_id"; // Error: 'id' is readonly
// report.createdAt = new Date(); // Error: 'createdAt' is readonly
// report.author = "Different Author"; // Error: 'author' is readonly
// report.tags = ["new", "tags"]; // Error: 'tags' is readonly
// logo.designTool = "Photoshop"; // Error: 'designTool' is readonly
π― Access Modifier Patterns
ποΈ Builder Pattern with Access Control
// π House builder with controlled access
class House {
// π Readonly house specifications
public readonly id: string;
public readonly address: string;
// π Private construction details
private rooms: string[];
private squareFootage: number;
private isCompleted: boolean;
// π₯ Protected for subclasses
protected buildingMaterials: string[];
protected constructionCost: number;
private constructor(
address: string,
squareFootage: number
) {
this.id = `HOUSE_${Date.now()}`;
this.address = address;
this.squareFootage = squareFootage;
this.rooms = [];
this.isCompleted = false;
this.buildingMaterials = [];
this.constructionCost = 0;
}
// π Static factory method for controlled creation
public static startConstruction(address: string, squareFootage: number): HouseBuilder {
return new HouseBuilder(new House(address, squareFootage));
}
// π Private method to add room
private addRoom(room: string): void {
if (this.isCompleted) {
throw new Error("β Cannot add rooms to completed house");
}
this.rooms.push(room);
}
// π₯ Protected method for subclasses
protected calculateBaseCost(): number {
return this.squareFootage * 100;
}
// π Public getters
public getAddress(): string {
return this.address;
}
public getRooms(): readonly string[] {
return [...this.rooms];
}
public getSquareFootage(): number {
return this.squareFootage;
}
public isReady(): boolean {
return this.isCompleted;
}
// π Public method to get house info
public getHouseInfo(): string {
const status = this.isCompleted ? "β
Completed" : "π§ Under Construction";
return `
π House ${this.id}
π Address: ${this.address}
π Square Footage: ${this.squareFootage} sq ft
π Rooms: ${this.rooms.join(", ") || "None yet"}
π Status: ${status}
π° Total Cost: $${this.constructionCost.toLocaleString()}
`.trim();
}
}
// π¨ Builder class with controlled access to House
class HouseBuilder {
private house: House;
// π Private constructor - only House can create
constructor(house: House) {
this.house = house;
}
// π Add rooms through builder
public addBedroom(): this {
(this.house as any).addRoom("Bedroom"); // Type assertion to access private
(this.house as any).constructionCost += 15000;
console.log("ποΈ Added bedroom");
return this;
}
public addBathroom(): this {
(this.house as any).addRoom("Bathroom");
(this.house as any).constructionCost += 10000;
console.log("πΏ Added bathroom");
return this;
}
public addKitchen(): this {
(this.house as any).addRoom("Kitchen");
(this.house as any).constructionCost += 25000;
console.log("π³ Added kitchen");
return this;
}
public addLivingRoom(): this {
(this.house as any).addRoom("Living Room");
(this.house as any).constructionCost += 20000;
console.log("ποΈ Added living room");
return this;
}
// ποΈ Complete construction
public completeConstruction(): House {
if ((this.house as any).rooms.length === 0) {
throw new Error("β House must have at least one room");
}
(this.house as any).isCompleted = true;
console.log("π House construction completed!");
return this.house;
}
}
// π‘ Luxury House with additional features
class LuxuryHouse extends House {
// π Private luxury features
private luxuryFeatures: string[];
constructor(address: string, squareFootage: number) {
// β Can't call private constructor directly
// super(address, squareFootage); // Error!
// Must use static factory method from parent
const tempHouse = House.startConstruction(address, squareFootage).completeConstruction();
// Copy properties (in real implementation, you'd handle this differently)
Object.assign(this, tempHouse);
this.luxuryFeatures = [];
console.log("β¨ Luxury house features initialized");
}
// π― Luxury-specific methods
public addLuxuryFeature(feature: string, cost: number): void {
this.luxuryFeatures.push(feature);
this.constructionCost += cost; // β
Can access protected property
console.log(`β¨ Added luxury feature: ${feature} (+$${cost.toLocaleString()})`);
}
// π Override with luxury details
public getHouseInfo(): string {
const baseInfo = super.getHouseInfo();
const luxuryInfo = this.luxuryFeatures.length > 0
? `\n⨠Luxury Features: ${this.luxuryFeatures.join(", ")}`
: "";
return baseInfo + luxuryInfo;
}
}
// π Test access control in building pattern
console.log("ποΈ Building a house...\n");
const myHouse = House.startConstruction("123 Main St", 2000)
.addBedroom()
.addBedroom()
.addBathroom()
.addKitchen()
.addLivingRoom()
.completeConstruction();
console.log("\n" + myHouse.getHouseInfo());
// β These would cause errors:
// const directHouse = new House("456 Oak St", 1500); // Error: constructor is private
// myHouse.addRoom("Garage"); // Error: addRoom is private
// console.log(myHouse.rooms); // Error: rooms is private
β οΈ Common Pitfalls and Solutions
π± Pitfall 1: Overusing private
// β Too restrictive - hard to extend or test
class BadBankAccount {
private balance: number = 0;
private transactions: string[] = [];
private validateAmount(amount: number): boolean {
return amount > 0;
}
private addTransaction(description: string): void {
this.transactions.push(description);
}
// No way for subclasses to access or extend functionality
}
// β
Better balance of access control
class GoodBankAccount {
private balance: number = 0;
protected transactions: string[] = []; // Subclasses can access
protected validateAmount(amount: number): boolean { // Subclasses can override
return amount > 0;
}
protected addTransaction(description: string): void { // Subclasses can use
this.transactions.push(description);
}
// Clear public interface
public deposit(amount: number): boolean {
if (this.validateAmount(amount)) {
this.balance += amount;
this.addTransaction(`Deposit: +$${amount}`);
return true;
}
return false;
}
}
π€― Pitfall 2: Readonly confusion
// β Misunderstanding readonly behavior
class BadExample {
public readonly items: string[] = [];
public readonly config: { mode: string } = { mode: "development" };
updateItems(): void {
// β Think this won't work, but it does!
this.items.push("new item"); // β
Array contents can change
this.config.mode = "production"; // β
Object properties can change
// These are the operations that would fail:
// this.items = []; // β Can't reassign array reference
// this.config = { mode: "prod" }; // β Can't reassign object reference
}
}
// β
Proper readonly usage
class GoodExample {
public readonly items: ReadonlyArray<string> = []; // Truly immutable array
public readonly config: Readonly<{ mode: string }> = { mode: "development" };
updateItems(newItem: string): void {
// β
Create new array instead of mutating
(this as any).items = [...this.items, newItem];
}
}
π οΈ Best Practices
- π― Start Private: Begin with private, then expose as needed
- π₯ Use Protected Wisely: For inheritance-friendly APIs
- π Minimal Public Interface: Only expose what users need
- π Readonly for Immutability: Use for data that shouldnβt change
- π Encapsulation First: Hide implementation details
- π Consistent Patterns: Use same access patterns throughout
π§ͺ Hands-On Exercise
π― Challenge: Build a Secure Task Management System
Create a comprehensive task management system with proper access control:
π Requirements:
- π Task class with appropriate access modifiers
- π€ User class with protected user data
- π’ TaskManager class with private task storage
- π Permission system with different access levels
- π Audit trail with readonly history
- π₯ Team management with inheritance
π Bonus Features:
- Role-based access control
- Task encryption for sensitive tasks
- Performance metrics with readonly data
- Notification system with protected methods
π‘ Solution
π Click to see solution
// π Task class with comprehensive access control
class Task {
// π Readonly properties that never change
public readonly id: string;
public readonly createdAt: Date;
public readonly createdBy: string;
// π Public properties for general access
public title: string;
public description: string;
public priority: "low" | "medium" | "high";
public status: "pending" | "in-progress" | "completed" | "cancelled";
// π₯ Protected properties for subclasses and managers
protected assignedTo: string | null;
protected dueDate: Date | null;
protected category: string;
// π Private properties for internal use only
private isEncrypted: boolean;
private encryptedData: string | null;
private lastModified: Date;
private modificationHistory: string[];
constructor(
title: string,
description: string,
createdBy: string,
priority: "low" | "medium" | "high" = "medium"
) {
this.id = `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
this.createdAt = new Date();
this.createdBy = createdBy;
this.title = title;
this.description = description;
this.priority = priority;
this.status = "pending";
this.assignedTo = null;
this.dueDate = null;
this.category = "general";
this.isEncrypted = false;
this.encryptedData = null;
this.lastModified = new Date();
this.modificationHistory = [`Created by ${createdBy}`];
console.log(`π Task "${title}" created with ID: ${this.id}`);
}
// π Private method for adding to history
private addToHistory(action: string, user: string): void {
const timestamp = new Date().toISOString();
this.modificationHistory.push(`${timestamp}: ${action} by ${user}`);
this.lastModified = new Date();
}
// π Private encryption methods
private encrypt(data: string): string {
// Simple encryption for demo (use proper encryption in production)
return Buffer.from(data).toString('base64');
}
private decrypt(encryptedData: string): string {
return Buffer.from(encryptedData, 'base64').toString();
}
// π₯ Protected method for managers and subclasses
protected validatePermission(user: string, action: string): boolean {
// Basic permission check - can be overridden by subclasses
return user === this.createdBy || user === this.assignedTo;
}
// π Public method to assign task
public assignTo(user: string, assignedBy: string): boolean {
if (!this.validatePermission(assignedBy, "assign")) {
console.log(`β ${assignedBy} doesn't have permission to assign this task`);
return false;
}
this.assignedTo = user;
this.addToHistory(`Assigned to ${user}`, assignedBy);
console.log(`π€ Task "${this.title}" assigned to ${user}`);
return true;
}
// π Public method to update status
public updateStatus(newStatus: typeof this.status, user: string): boolean {
if (!this.validatePermission(user, "update")) {
console.log(`β ${user} doesn't have permission to update this task`);
return false;
}
const oldStatus = this.status;
this.status = newStatus;
this.addToHistory(`Status changed from ${oldStatus} to ${newStatus}`, user);
console.log(`π Task "${this.title}" status updated to ${newStatus}`);
return true;
}
// π Public method to set due date
public setDueDate(dueDate: Date, user: string): boolean {
if (!this.validatePermission(user, "update")) {
console.log(`β ${user} doesn't have permission to set due date`);
return false;
}
this.dueDate = dueDate;
this.addToHistory(`Due date set to ${dueDate.toDateString()}`, user);
console.log(`π
Due date set for "${this.title}": ${dueDate.toDateString()}`);
return true;
}
// π Public method to encrypt sensitive tasks
public encryptTask(user: string): boolean {
if (user !== this.createdBy) {
console.log(`β Only task creator can encrypt tasks`);
return false;
}
if (this.isEncrypted) {
console.log(`π Task is already encrypted`);
return false;
}
this.encryptedData = this.encrypt(JSON.stringify({
title: this.title,
description: this.description
}));
this.title = "[ENCRYPTED]";
this.description = "[ENCRYPTED]";
this.isEncrypted = true;
this.addToHistory("Task encrypted", user);
console.log(`π Task encrypted by ${user}`);
return true;
}
// π Public method to decrypt tasks
public decryptTask(user: string): boolean {
if (user !== this.createdBy) {
console.log(`β Only task creator can decrypt tasks`);
return false;
}
if (!this.isEncrypted || !this.encryptedData) {
console.log(`π Task is not encrypted`);
return false;
}
const decryptedData = JSON.parse(this.decrypt(this.encryptedData));
this.title = decryptedData.title;
this.description = decryptedData.description;
this.isEncrypted = false;
this.encryptedData = null;
this.addToHistory("Task decrypted", user);
console.log(`π Task decrypted by ${user}`);
return true;
}
// π Public getters
public getAssignedTo(): string | null {
return this.assignedTo;
}
public getDueDate(): Date | null {
return this.dueDate;
}
public getCategory(): string {
return this.category;
}
public isOverdue(): boolean {
if (!this.dueDate) return false;
return new Date() > this.dueDate && this.status !== "completed";
}
// π Public method to get task info
public getTaskInfo(requestingUser: string): string {
const isAuthorized = this.validatePermission(requestingUser, "view");
if (this.isEncrypted && !isAuthorized) {
return `π Task ${this.id} - [ENCRYPTED] - Access Denied`;
}
const dueInfo = this.dueDate ? `π
Due: ${this.dueDate.toDateString()}` : "π
No due date";
const assigneeInfo = this.assignedTo ? `π€ Assigned: ${this.assignedTo}` : "π€ Unassigned";
const overdueFlag = this.isOverdue() ? " β οΈ OVERDUE" : "";
return `
π ${this.title}${overdueFlag}
π ${this.description}
π ID: ${this.id}
π Status: ${this.status} | Priority: ${this.priority}
${assigneeInfo}
${dueInfo}
π€ Created by: ${this.createdBy} on ${this.createdAt.toDateString()}
π·οΈ Category: ${this.category}
`.trim();
}
// π Public method to get history (limited access)
public getHistory(requestingUser: string): string[] | null {
if (!this.validatePermission(requestingUser, "view")) {
console.log(`β ${requestingUser} doesn't have permission to view task history`);
return null;
}
return [...this.modificationHistory]; // Return copy
}
}
// π― Priority Task with additional security
class PriorityTask extends Task {
// π Private priority-specific properties
private stakeholders: string[];
private approvalRequired: boolean;
private approvedBy: string | null;
constructor(
title: string,
description: string,
createdBy: string,
stakeholders: string[] = []
) {
super(title, description, createdBy, "high");
this.stakeholders = stakeholders;
this.approvalRequired = true;
this.approvedBy = null;
this.category = "priority";
console.log(`β Priority task created with ${stakeholders.length} stakeholders`);
}
// π₯ Override permission validation for priority tasks
protected validatePermission(user: string, action: string): boolean {
// Priority tasks have stricter permissions
const isCreator = user === this.createdBy;
const isAssignee = user === this.assignedTo;
const isStakeholder = this.stakeholders.includes(user);
switch (action) {
case "assign":
case "approve":
return isCreator || isStakeholder;
case "update":
return (isCreator || isAssignee) && (this.approvedBy !== null || !this.approvalRequired);
case "view":
return isCreator || isAssignee || isStakeholder;
default:
return super.validatePermission(user, action);
}
}
// π Public method to approve priority task
public approve(user: string): boolean {
if (!this.validatePermission(user, "approve")) {
console.log(`β ${user} doesn't have permission to approve this priority task`);
return false;
}
this.approvedBy = user;
this.approvalRequired = false;
console.log(`β
Priority task "${this.title}" approved by ${user}`);
return true;
}
// π Get priority task info with additional details
public getTaskInfo(requestingUser: string): string {
const baseInfo = super.getTaskInfo(requestingUser);
if (!this.validatePermission(requestingUser, "view")) {
return baseInfo;
}
const approvalStatus = this.approvedBy
? `β
Approved by ${this.approvedBy}`
: "β³ Pending approval";
const stakeholderList = this.stakeholders.length > 0
? `π₯ Stakeholders: ${this.stakeholders.join(", ")}`
: "π₯ No stakeholders";
return `${baseInfo}\nβ PRIORITY TASK\n${approvalStatus}\n${stakeholderList}`;
}
}
// π€ User class with protected data
class User {
// π Readonly user identification
public readonly id: string;
public readonly username: string;
public readonly email: string;
// π₯ Protected user data for subclasses
protected role: "user" | "manager" | "admin";
protected permissions: string[];
protected lastLogin: Date | null;
// π Private sensitive data
private hashedPassword: string;
private loginAttempts: number;
private isLocked: boolean;
constructor(username: string, email: string, password: string) {
this.id = `user_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
this.username = username;
this.email = email;
this.role = "user";
this.permissions = ["view_own_tasks", "create_tasks"];
this.lastLogin = null;
this.hashedPassword = this.hashPassword(password);
this.loginAttempts = 0;
this.isLocked = false;
console.log(`π€ User ${username} created with ID: ${this.id}`);
}
// π Private password hashing
private hashPassword(password: string): string {
// Simple hash for demo (use proper hashing in production)
return Buffer.from(password).toString('base64');
}
// π₯ Protected method for authentication
protected authenticate(password: string): boolean {
if (this.isLocked) {
console.log(`π Account ${this.username} is locked`);
return false;
}
const hashedInput = this.hashPassword(password);
if (hashedInput === this.hashedPassword) {
this.loginAttempts = 0;
this.lastLogin = new Date();
return true;
}
this.loginAttempts++;
if (this.loginAttempts >= 3) {
this.isLocked = true;
console.log(`π Account ${this.username} locked due to too many failed attempts`);
}
return false;
}
// π Public login method
public login(password: string): boolean {
if (this.authenticate(password)) {
console.log(`β
${this.username} logged in successfully`);
return true;
}
console.log(`β Login failed for ${this.username}`);
return false;
}
// π Public method to check permissions
public hasPermission(permission: string): boolean {
return this.permissions.includes(permission);
}
// π Public getters
public getRole(): string {
return this.role;
}
public getLastLogin(): Date | null {
return this.lastLogin;
}
// π Public user info
public getUserInfo(): string {
const lastLoginStr = this.lastLogin
? this.lastLogin.toLocaleString()
: "Never";
return `
π€ ${this.username} (${this.email})
π ID: ${this.id}
π€ Role: ${this.role}
π Permissions: ${this.permissions.join(", ")}
β° Last Login: ${lastLoginStr}
`.trim();
}
}
// π Manager class with additional privileges
class Manager extends User {
// π₯ Protected team management
protected teamMembers: string[];
protected managedProjects: string[];
constructor(username: string, email: string, password: string) {
super(username, email, password);
this.role = "manager";
this.permissions = [
...this.permissions,
"view_all_tasks",
"assign_tasks",
"approve_tasks",
"manage_team"
];
this.teamMembers = [];
this.managedProjects = [];
console.log(`π Manager ${username} account created`);
}
// π Public method to add team member
public addTeamMember(userId: string): void {
if (!this.teamMembers.includes(userId)) {
this.teamMembers.push(userId);
console.log(`π₯ Added team member: ${userId}`);
}
}
// π Public method to check if user is team member
public isTeamMember(userId: string): boolean {
return this.teamMembers.includes(userId);
}
}
// π Demo the secure task management system
console.log("π’ Setting up Task Management System...\n");
// Create users
const alice = new User("alice", "[email protected]", "password123");
const bob = new Manager("bob", "[email protected]", "manager456");
const charlie = new User("charlie", "[email protected]", "user789");
// Login users
alice.login("password123");
bob.login("manager456");
// Add team members
bob.addTeamMember(alice.id);
// Create tasks
const task1 = new Task(
"Implement user authentication",
"Add secure login system with password hashing",
alice.username
);
const priorityTask = new PriorityTask(
"Security audit",
"Comprehensive security review of the system",
bob.username,
[alice.username, charlie.username]
);
console.log("\nπ Task Operations:");
// Task operations
task1.assignTo(charlie.username, alice.username);
task1.setDueDate(new Date("2025-07-01"), alice.username);
task1.updateStatus("in-progress", charlie.username);
// Priority task operations
priorityTask.approve(alice.username);
priorityTask.assignTo(charlie.username, bob.username);
console.log("\nπ Task Information:");
console.log(task1.getTaskInfo(alice.username));
console.log("\n" + priorityTask.getTaskInfo(charlie.username));
console.log("\nπ€ User Information:");
console.log(alice.getUserInfo());
console.log("\n" + bob.getUserInfo());
// Test encryption
console.log("\nπ Testing Encryption:");
task1.encryptTask(alice.username);
console.log("After encryption:");
console.log(task1.getTaskInfo(charlie.username)); // Should show limited info
task1.decryptTask(alice.username);
console.log("After decryption:");
console.log(task1.getTaskInfo(alice.username));
π Key Takeaways
Outstanding! Youβve mastered TypeScript access modifiers! Hereβs what you can now do:
- β Use public effectively for clean APIs π
- β Secure with private for internal implementation π
- β Share with protected for inheritance-friendly design π₯
- β Protect with readonly for immutable data π
- β Design secure systems with proper encapsulation π‘οΈ
- β Create maintainable hierarchies with controlled access ποΈ
Remember: Good access control is the foundation of secure, maintainable code! π
π€ Next Steps
Excellent work! π Youβre now an access control expert!
Continue building your TypeScript mastery:
- π» Complete the secure task management exercise above
- ποΈ Refactor existing code to use proper access modifiers
- π Next tutorial: Static Members and Methods - Class-Level Functionality
- π Design your own secure class hierarchies!
Remember: Great software is built with careful attention to access control. Keep securing, keep learning! π
Happy coding! ππβ¨