+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 32 of 355

๐Ÿ“˜ Getters and Setters: Property Access Control

๐ŸŽฏ Master TypeScript getters and setters to create smart properties with validation, computed values, and side effects. Level up your OOP skills! ๐Ÿš€

๐Ÿš€Intermediate
25 min read

Prerequisites

  • โœ… Understanding of TypeScript classes
  • ๐Ÿ“š Basic knowledge of properties and methods
  • ๐Ÿ”ง Familiarity with access modifiers

What you'll learn

  • ๐ŸŽฏ Master getter and setter syntax in TypeScript
  • ๐Ÿ›ก๏ธ Implement property validation and constraints
  • ๐Ÿ’ก Create computed and derived properties
  • ๐Ÿš€ Apply best practices for property encapsulation

๐ŸŽฏ Introduction

Welcome to the world of getters and setters in TypeScript! ๐ŸŽ‰ Have you ever wanted your object properties to be smarter? To validate data before storing it? To compute values on the fly? Thatโ€™s exactly what getters and setters are for! ๐Ÿง™โ€โ™‚๏ธ

Think of getters and setters as smart gatekeepers ๐Ÿšช for your object properties. Instead of letting anyone directly mess with your data, they control access, validate input, and can even trigger side effects. Letโ€™s unlock this powerful feature! ๐Ÿ”“

๐Ÿ“š Understanding Getters and Setters

๐ŸŽญ What Are Getters and Setters?

Getters and setters are special methods that look and act like properties:

  • Getter ๐Ÿ“ค: A method that retrieves a property value
  • Setter ๐Ÿ“ฅ: A method that sets a property value
class Temperature {
  private _celsius: number = 0;
  
  // Getter - looks like a property when reading ๐Ÿ“–
  get celsius(): number {
    console.log("๐ŸŒก๏ธ Getting temperature in Celsius");
    return this._celsius;
  }
  
  // Setter - looks like a property when writing โœ๏ธ
  set celsius(value: number) {
    console.log("๐ŸŒก๏ธ Setting temperature in Celsius");
    if (value < -273.15) {
      throw new Error("โ„๏ธ Temperature below absolute zero!");
    }
    this._celsius = value;
  }
  
  // Computed getter - no backing field needed! ๐Ÿงฎ
  get fahrenheit(): number {
    return (this._celsius * 9/5) + 32;
  }
  
  set fahrenheit(value: number) {
    this.celsius = (value - 32) * 5/9;
  }
}

// Usage looks just like properties! โœจ
const temp = new Temperature();
temp.celsius = 25;           // Calls the setter
console.log(temp.celsius);   // Calls the getter: 25
console.log(temp.fahrenheit); // Computed on the fly: 77
temp.fahrenheit = 86;        // Sets celsius to 30

๐Ÿ” Why Use Getters and Setters?

Traditional public properties vs. smart properties:

// โŒ Traditional approach - no control!
class BankAccountBad {
  balance: number = 0; // Anyone can set this to anything! ๐Ÿ˜ฑ
}

const badAccount = new BankAccountBad();
badAccount.balance = -1000000; // Uh oh! ๐Ÿ’ธ

// โœ… Smart approach with getters/setters
class BankAccountGood {
  private _balance: number = 0;
  private _transactions: string[] = [];
  
  get balance(): number {
    return this._balance;
  }
  
  set balance(amount: number) {
    if (amount < 0) {
      throw new Error("๐Ÿšซ Balance cannot be negative!");
    }
    const difference = amount - this._balance;
    this._balance = amount;
    this._transactions.push(`๐Ÿ’ฐ Balance adjusted by ${difference}`);
  }
  
  get transactionHistory(): string[] {
    return [...this._transactions]; // Return a copy! ๐Ÿ›ก๏ธ
  }
}

const goodAccount = new BankAccountGood();
goodAccount.balance = 1000;    // โœ… Valid
// goodAccount.balance = -500;  // ๐Ÿšซ Throws error!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Getter Syntax

Getters use the get keyword:

class User {
  private _firstName: string;
  private _lastName: string;
  private _birthYear: number;
  
  constructor(firstName: string, lastName: string, birthYear: number) {
    this._firstName = firstName;
    this._lastName = lastName;
    this._birthYear = birthYear;
  }
  
  // Simple getter ๐Ÿ“ค
  get firstName(): string {
    return this._firstName;
  }
  
  // Computed getter - combines values ๐Ÿ”—
  get fullName(): string {
    return `${this._firstName} ${this._lastName}`;
  }
  
  // Computed getter - calculates on the fly ๐Ÿงฎ
  get age(): number {
    return new Date().getFullYear() - this._birthYear;
  }
  
  // Formatted getter - returns processed data ๐ŸŽจ
  get initials(): string {
    return `${this._firstName[0]}.${this._lastName[0]}.`.toUpperCase();
  }
}

const user = new User("John", "Doe", 1990);
console.log(user.fullName);  // John Doe
console.log(user.age);       // 35 (in 2025)
console.log(user.initials);  // J.D.

๐Ÿ“ Setter Syntax

Setters use the set keyword and must have exactly one parameter:

class Product {
  private _name: string = "";
  private _price: number = 0;
  private _discount: number = 0;
  private _lastModified: Date = new Date();
  
  // Setter with validation ๐Ÿ›ก๏ธ
  set name(value: string) {
    if (value.trim().length === 0) {
      throw new Error("๐Ÿšซ Product name cannot be empty!");
    }
    this._name = value.trim();
    this._lastModified = new Date();
  }
  
  get name(): string {
    return this._name;
  }
  
  // Setter with constraints ๐Ÿ“
  set price(value: number) {
    if (value < 0) {
      throw new Error("๐Ÿšซ Price cannot be negative!");
    }
    this._price = Math.round(value * 100) / 100; // Round to 2 decimals
    this._lastModified = new Date();
  }
  
  get price(): number {
    return this._price;
  }
  
  // Setter with business logic ๐Ÿ’ผ
  set discount(percentage: number) {
    if (percentage < 0 || percentage > 100) {
      throw new Error("๐Ÿšซ Discount must be between 0 and 100!");
    }
    this._discount = percentage;
    console.log(`๐ŸŽ‰ Discount of ${percentage}% applied!`);
  }
  
  get finalPrice(): number {
    return this._price * (1 - this._discount / 100);
  }
  
  get lastModified(): string {
    return this._lastModified.toLocaleString();
  }
}

const product = new Product();
product.name = "  Gaming Laptop  "; // Trimmed automatically
product.price = 999.999;           // Rounded to 999.99
product.discount = 20;             // 20% off

console.log(product.finalPrice);   // 799.99
console.log(product.lastModified); // Current timestamp

๐Ÿ’ก Practical Examples

๐Ÿ›ก๏ธ Example 1: Form Validation System

Letโ€™s build a form field with built-in validation:

class FormField<T> {
  private _value: T | null = null;
  private _errors: string[] = [];
  private _touched: boolean = false;
  private _validators: Array<(value: T) => string | null> = [];
  
  constructor(
    private _name: string,
    private _defaultValue: T,
    validators: Array<(value: T) => string | null> = []
  ) {
    this._value = _defaultValue;
    this._validators = validators;
  }
  
  // Getter for current value ๐Ÿ“ค
  get value(): T {
    return this._value ?? this._defaultValue;
  }
  
  // Setter with validation ๐Ÿ“ฅ
  set value(newValue: T) {
    this._touched = true;
    this._errors = [];
    
    // Run all validators ๐Ÿƒโ€โ™‚๏ธ
    for (const validator of this._validators) {
      const error = validator(newValue);
      if (error) {
        this._errors.push(error);
      }
    }
    
    // Only update if valid โœ…
    if (this._errors.length === 0) {
      this._value = newValue;
      console.log(`โœ… ${this._name} updated to: ${newValue}`);
    } else {
      console.log(`โŒ ${this._name} validation failed!`);
    }
  }
  
  // Computed getters ๐Ÿงฎ
  get isValid(): boolean {
    return this._errors.length === 0;
  }
  
  get isDirty(): boolean {
    return this._value !== this._defaultValue;
  }
  
  get hasErrors(): boolean {
    return this._touched && this._errors.length > 0;
  }
  
  get errors(): string[] {
    return [...this._errors];
  }
  
  get status(): string {
    if (!this._touched) return "โšช Untouched";
    if (this.hasErrors) return "๐Ÿ”ด Invalid";
    if (this.isDirty) return "๐ŸŸก Modified";
    return "๐ŸŸข Valid";
  }
}

// Email validation example ๐Ÿ“ง
const emailValidators = [
  (value: string) => value.length === 0 ? "Email is required" : null,
  (value: string) => !value.includes("@") ? "Invalid email format" : null,
  (value: string) => value.length < 5 ? "Email too short" : null,
];

const emailField = new FormField("Email", "", emailValidators);

console.log(emailField.status); // โšช Untouched

emailField.value = "";          // โŒ Email validation failed!
console.log(emailField.errors); // ["Email is required"]

emailField.value = "test";      // โŒ Email validation failed!
console.log(emailField.errors); // ["Invalid email format"]

emailField.value = "[email protected]"; // โœ… Email updated
console.log(emailField.status); // ๐ŸŸก Modified
console.log(emailField.isValid); // true

๐Ÿ’ฐ Example 2: Shopping Cart with Smart Properties

class ShoppingCart {
  private _items: Map<string, {product: string, price: number, quantity: number}> = new Map();
  private _couponCode: string | null = null;
  private _couponDiscount: number = 0;
  
  // Add item method (for setup) ๐Ÿ›๏ธ
  addItem(id: string, product: string, price: number, quantity: number = 1): void {
    const existing = this._items.get(id);
    if (existing) {
      existing.quantity += quantity;
    } else {
      this._items.set(id, { product, price, quantity });
    }
    console.log(`๐Ÿ›’ Added ${quantity}x ${product} to cart`);
  }
  
  // Computed getter - subtotal ๐Ÿ’ต
  get subtotal(): number {
    let total = 0;
    this._items.forEach(item => {
      total += item.price * item.quantity;
    });
    return Math.round(total * 100) / 100;
  }
  
  // Computed getter - tax ๐Ÿ’ธ
  get tax(): number {
    const TAX_RATE = 0.08; // 8% tax
    return Math.round(this.subtotal * TAX_RATE * 100) / 100;
  }
  
  // Computed getter - discount amount ๐ŸŽ
  get discountAmount(): number {
    return Math.round(this.subtotal * this._couponDiscount * 100) / 100;
  }
  
  // Computed getter - total ๐Ÿ’ฐ
  get total(): number {
    return this.subtotal + this.tax - this.discountAmount;
  }
  
  // Setter with coupon validation ๐ŸŽŸ๏ธ
  set couponCode(code: string | null) {
    if (!code) {
      this._couponCode = null;
      this._couponDiscount = 0;
      console.log("๐ŸŽŸ๏ธ Coupon removed");
      return;
    }
    
    // Simulate coupon validation
    const validCoupons: Record<string, number> = {
      "SAVE10": 0.10,
      "SAVE20": 0.20,
      "HALFOFF": 0.50,
    };
    
    const discount = validCoupons[code.toUpperCase()];
    if (discount) {
      this._couponCode = code.toUpperCase();
      this._couponDiscount = discount;
      console.log(`โœ… Coupon ${code} applied! ${discount * 100}% off`);
    } else {
      throw new Error(`โŒ Invalid coupon code: ${code}`);
    }
  }
  
  get couponCode(): string | null {
    return this._couponCode;
  }
  
  // Computed getter - savings message ๐Ÿ’ฌ
  get savingsMessage(): string {
    if (this.discountAmount > 0) {
      return `๐ŸŽ‰ You saved $${this.discountAmount.toFixed(2)} with coupon ${this._couponCode}!`;
    }
    return "๐Ÿ’ก Add a coupon code to save!";
  }
  
  // Computed getter - item count ๐Ÿ“Š
  get itemCount(): number {
    let count = 0;
    this._items.forEach(item => count += item.quantity);
    return count;
  }
  
  // Computed getter - summary ๐Ÿ“‹
  get summary(): string {
    const lines = [
      "๐Ÿ›’ Shopping Cart Summary",
      "โ”€".repeat(30),
      `Items: ${this.itemCount}`,
      `Subtotal: $${this.subtotal.toFixed(2)}`,
    ];
    
    if (this._couponCode) {
      lines.push(`Discount (${this._couponCode}): -$${this.discountAmount.toFixed(2)}`);
    }
    
    lines.push(
      `Tax: $${this.tax.toFixed(2)}`,
      "โ”€".repeat(30),
      `Total: $${this.total.toFixed(2)}`,
      "",
      this.savingsMessage
    );
    
    return lines.join("\n");
  }
}

// Using the smart shopping cart ๐Ÿ›๏ธ
const cart = new ShoppingCart();
cart.addItem("1", "Gaming Mouse", 79.99, 1);
cart.addItem("2", "Mechanical Keyboard", 149.99, 1);
cart.addItem("3", "USB Cable", 9.99, 2);

console.log(cart.summary);
/*
๐Ÿ›’ Shopping Cart Summary
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Items: 4
Subtotal: $249.97
Tax: $20.00
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Total: $269.97

๐Ÿ’ก Add a coupon code to save!
*/

cart.couponCode = "SAVE20";
console.log(cart.summary);
/*
๐Ÿ›’ Shopping Cart Summary
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Items: 4
Subtotal: $249.97
Discount (SAVE20): -$49.99
Tax: $20.00
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Total: $219.98

๐ŸŽ‰ You saved $49.99 with coupon SAVE20!
*/

๐ŸŽฎ Example 3: Game Character with Dynamic Stats

class GameCharacter {
  private _level: number = 1;
  private _experience: number = 0;
  private _baseStrength: number = 10;
  private _baseDefense: number = 10;
  private _equipment: Map<string, {slot: string, bonus: number}> = new Map();
  private _statusEffects: Set<string> = new Set();
  
  constructor(public name: string) {}
  
  // Experience setter with auto-leveling ๐Ÿ“ˆ
  set experience(value: number) {
    if (value < 0) value = 0;
    this._experience = value;
    
    // Auto level up! ๐ŸŽŠ
    const newLevel = Math.floor(this._experience / 1000) + 1;
    if (newLevel > this._level) {
      const levelsGained = newLevel - this._level;
      this._level = newLevel;
      this._baseStrength += levelsGained * 2;
      this._baseDefense += levelsGained * 2;
      console.log(`๐ŸŽ‰ ${this.name} leveled up to ${this._level}!`);
    }
  }
  
  get experience(): number {
    return this._experience;
  }
  
  get level(): number {
    return this._level;
  }
  
  // Computed stat with equipment bonuses ๐Ÿ’ช
  get strength(): number {
    let total = this._baseStrength;
    
    // Add equipment bonuses
    this._equipment.forEach(item => {
      if (item.slot === "weapon" || item.slot === "gloves") {
        total += item.bonus;
      }
    });
    
    // Apply status effects
    if (this._statusEffects.has("strengthBoost")) {
      total = Math.floor(total * 1.5);
    }
    if (this._statusEffects.has("weakened")) {
      total = Math.floor(total * 0.7);
    }
    
    return total;
  }
  
  // Computed defense with equipment ๐Ÿ›ก๏ธ
  get defense(): number {
    let total = this._baseDefense;
    
    // Add equipment bonuses
    this._equipment.forEach(item => {
      if (item.slot === "armor" || item.slot === "shield") {
        total += item.bonus;
      }
    });
    
    // Apply status effects
    if (this._statusEffects.has("defenseBoost")) {
      total = Math.floor(total * 1.5);
    }
    if (this._statusEffects.has("vulnerable")) {
      total = Math.floor(total * 0.5);
    }
    
    return total;
  }
  
  // Combat power calculation ๐Ÿ”ฅ
  get combatPower(): number {
    return this.strength * 2 + this.defense + this.level * 10;
  }
  
  // Status effect management ๐ŸŽญ
  set statusEffect(effect: string) {
    this._statusEffects.add(effect);
    console.log(`๐Ÿ“ ${this.name} gained ${effect} effect!`);
  }
  
  removeStatusEffect(effect: string): void {
    if (this._statusEffects.delete(effect)) {
      console.log(`๐Ÿ“ค ${this.name} lost ${effect} effect!`);
    }
  }
  
  // Equipment management ๐ŸŽฝ
  equipItem(id: string, slot: string, name: string, bonus: number): void {
    this._equipment.set(id, { slot, bonus });
    console.log(`โš”๏ธ ${this.name} equipped ${name} (+${bonus} to ${slot})`);
  }
  
  // Character sheet getter ๐Ÿ“Š
  get characterSheet(): string {
    return `
๐ŸŽฎ Character: ${this.name}
๐Ÿ“Š Level: ${this.level} (${this.experience} XP)
๐Ÿ’ช Strength: ${this.strength}
๐Ÿ›ก๏ธ Defense: ${this.defense}
๐Ÿ”ฅ Combat Power: ${this.combatPower}
๐ŸŽญ Active Effects: ${Array.from(this._statusEffects).join(", ") || "None"}
    `.trim();
  }
}

// Create and play with character ๐ŸŽฎ
const hero = new GameCharacter("Aragorn");

console.log(hero.characterSheet);

// Gain experience and level up!
hero.experience = 2500; // Will level up to 3

// Equip items
hero.equipItem("sword1", "weapon", "Legendary Sword", 15);
hero.equipItem("armor1", "armor", "Dragon Scale Armor", 20);

// Apply status effects
hero.statusEffect = "strengthBoost";

console.log(hero.characterSheet);

๐Ÿš€ Advanced Concepts

๐Ÿ” Private Setters and Public Getters

Sometimes you want a property to be readable but not writable from outside:

class AuthToken {
  private _token: string | null = null;
  private _expiresAt: Date | null = null;
  private _refreshCount: number = 0;
  
  // Public getter, no public setter! ๐Ÿ”’
  get token(): string | null {
    if (this.isExpired) {
      console.log("โฐ Token expired!");
      return null;
    }
    return this._token;
  }
  
  get isExpired(): boolean {
    if (!this._expiresAt) return true;
    return new Date() > this._expiresAt;
  }
  
  get remainingTime(): number {
    if (!this._expiresAt || this.isExpired) return 0;
    return this._expiresAt.getTime() - Date.now();
  }
  
  get refreshCount(): number {
    return this._refreshCount;
  }
  
  // Internal method to set token ๐Ÿ”‘
  private setToken(token: string, expiresInMinutes: number): void {
    this._token = token;
    this._expiresAt = new Date(Date.now() + expiresInMinutes * 60 * 1000);
    console.log(`๐Ÿ” Token set, expires in ${expiresInMinutes} minutes`);
  }
  
  // Public methods that internally use setters
  async login(username: string, password: string): Promise<boolean> {
    // Simulate authentication
    if (username && password) {
      const fakeToken = `TOKEN_${Date.now()}_${Math.random()}`;
      this.setToken(fakeToken, 30); // 30 minutes
      console.log("โœ… Login successful!");
      return true;
    }
    return false;
  }
  
  async refresh(): Promise<boolean> {
    if (this._token && this.remainingTime > 0) {
      const newToken = `REFRESHED_${this._token}`;
      this.setToken(newToken, 30);
      this._refreshCount++;
      console.log(`๐Ÿ”„ Token refreshed (count: ${this._refreshCount})`);
      return true;
    }
    console.log("โŒ Cannot refresh expired token");
    return false;
  }
}

const auth = new AuthToken();
// auth.token = "fake"; // Error! No setter available ๐Ÿšซ

await auth.login("user", "pass");
console.log(auth.token); // TOKEN_...
console.log(`Time remaining: ${Math.floor(auth.remainingTime / 1000)}s`);

๐ŸŽฏ Lazy Initialization with Getters

Use getters for expensive computations that should be cached:

class DataAnalyzer {
  private _rawData: number[] = [];
  private _stats: {mean: number, median: number, mode: number} | null = null;
  private _sortedData: number[] | null = null;
  
  constructor(data: number[]) {
    this._rawData = [...data];
  }
  
  // Lazy-computed sorted data ๐Ÿฆฅ
  private get sortedData(): number[] {
    if (!this._sortedData) {
      console.log("๐Ÿ”„ Sorting data (expensive operation)...");
      this._sortedData = [...this._rawData].sort((a, b) => a - b);
    }
    return this._sortedData;
  }
  
  // Lazy-computed statistics ๐Ÿ“Š
  get statistics(): {mean: number, median: number, mode: number} {
    if (!this._stats) {
      console.log("๐Ÿงฎ Computing statistics (expensive operation)...");
      
      // Mean
      const mean = this._rawData.reduce((a, b) => a + b, 0) / this._rawData.length;
      
      // Median (using sorted data)
      const mid = Math.floor(this.sortedData.length / 2);
      const median = this.sortedData.length % 2 === 0
        ? (this.sortedData[mid - 1] + this.sortedData[mid]) / 2
        : this.sortedData[mid];
      
      // Mode
      const frequency: Record<number, number> = {};
      let maxFreq = 0;
      let mode = this._rawData[0];
      
      for (const num of this._rawData) {
        frequency[num] = (frequency[num] || 0) + 1;
        if (frequency[num] > maxFreq) {
          maxFreq = frequency[num];
          mode = num;
        }
      }
      
      this._stats = { mean, median, mode };
    }
    return this._stats;
  }
  
  // Setter that invalidates cache ๐Ÿ”„
  set data(newData: number[]) {
    this._rawData = [...newData];
    this._stats = null;      // Invalidate cached stats
    this._sortedData = null; // Invalidate sorted data
    console.log("๐Ÿ“ฅ Data updated, cache cleared");
  }
  
  get summary(): string {
    const stats = this.statistics;
    return `
๐Ÿ“Š Data Analysis:
- Count: ${this._rawData.length}
- Mean: ${stats.mean.toFixed(2)}
- Median: ${stats.median}
- Mode: ${stats.mode}
- Range: ${this.sortedData[this.sortedData.length - 1] - this.sortedData[0]}
    `.trim();
  }
}

const analyzer = new DataAnalyzer([5, 2, 8, 2, 9, 1, 2, 7]);
console.log(analyzer.summary); // First call computes everything
console.log(analyzer.summary); // Second call uses cache!

analyzer.data = [10, 20, 30]; // Update data
console.log(analyzer.summary); // Recomputes with new data

๐Ÿ”„ Setter Chaining

Enable fluent interfaces with setters:

class QueryBuilder {
  private _table: string = "";
  private _conditions: string[] = [];
  private _orderBy: string = "";
  private _limit: number | null = null;
  
  // Setters that return 'this' for chaining ๐Ÿ”—
  set table(name: string) {
    this._table = name;
  }
  
  get table(): string {
    return this._table;
  }
  
  // Alternative approach with methods for better chaining
  from(table: string): this {
    this._table = table;
    return this;
  }
  
  where(condition: string): this {
    this._conditions.push(condition);
    return this;
  }
  
  orderBy(column: string, direction: "ASC" | "DESC" = "ASC"): this {
    this._orderBy = `${column} ${direction}`;
    return this;
  }
  
  limit(count: number): this {
    this._limit = count;
    return this;
  }
  
  // Getter that builds the final query ๐Ÿ—๏ธ
  get query(): string {
    if (!this._table) {
      throw new Error("โŒ Table not specified!");
    }
    
    let query = `SELECT * FROM ${this._table}`;
    
    if (this._conditions.length > 0) {
      query += ` WHERE ${this._conditions.join(" AND ")}`;
    }
    
    if (this._orderBy) {
      query += ` ORDER BY ${this._orderBy}`;
    }
    
    if (this._limit) {
      query += ` LIMIT ${this._limit}`;
    }
    
    return query;
  }
  
  // Computed getter for query info ๐Ÿ“‹
  get queryInfo(): string {
    return `
๐Ÿ“‹ Query Builder Info:
- Table: ${this._table || "Not set"}
- Conditions: ${this._conditions.length}
- Order By: ${this._orderBy || "None"}
- Limit: ${this._limit || "None"}
    `.trim();
  }
}

// Fluent interface usage ๐ŸŒŠ
const query = new QueryBuilder()
  .from("users")
  .where("age > 18")
  .where("status = 'active'")
  .orderBy("created_at", "DESC")
  .limit(10);

console.log(query.query);
// SELECT * FROM users WHERE age > 18 AND status = 'active' ORDER BY created_at DESC LIMIT 10

โš ๏ธ Common Pitfalls and Solutions

๐Ÿšซ Pitfall 1: Infinite Loops in Setters

class BadExample {
  private _value: number = 0;
  
  // โŒ Wrong: Infinite recursion!
  set value(val: number) {
    this.value = val; // This calls the setter again! ๐Ÿ’ฅ
  }
  
  get value(): number {
    return this.value; // This calls the getter again! ๐Ÿ’ฅ
  }
}

class GoodExample {
  private _value: number = 0;
  
  // โœ… Correct: Use backing field
  set value(val: number) {
    this._value = val;
  }
  
  get value(): number {
    return this._value;
  }
}

๐Ÿšซ Pitfall 2: Missing Getter or Setter

class IncompleteProperty {
  private _data: string = "";
  
  // โŒ Only setter, no getter
  set data(value: string) {
    this._data = value;
  }
}

const obj = new IncompleteProperty();
obj.data = "Hello";
// console.log(obj.data); // Error! Property is write-only ๐Ÿšซ

class CompleteProperty {
  private _data: string = "";
  
  // โœ… Both getter and setter
  set data(value: string) {
    this._data = value;
  }
  
  get data(): string {
    return this._data;
  }
}

๐Ÿšซ Pitfall 3: Side Effects in Getters

// โŒ Bad: Getter with side effects
class BadCounter {
  private _count: number = 0;
  
  get count(): number {
    return ++this._count; // Modifies state! ๐Ÿ˜ฑ
  }
}

const bad = new BadCounter();
console.log(bad.count); // 1
console.log(bad.count); // 2 (unexpected!)

// โœ… Good: Pure getter
class GoodCounter {
  private _count: number = 0;
  
  get count(): number {
    return this._count;
  }
  
  increment(): void {
    this._count++;
  }
}

๐Ÿ› ๏ธ Best Practices

1๏ธโƒฃ Use Descriptive Names

class EmailService {
  private _recipientList: string[] = [];
  
  // โœ… Clear, descriptive names
  get recipients(): string[] {
    return [...this._recipientList];
  }
  
  set recipients(emails: string[]) {
    // Validate all emails
    const validEmails = emails.filter(email => this.isValidEmail(email));
    this._recipientList = validEmails;
  }
  
  get recipientCount(): number {
    return this._recipientList.length;
  }
  
  get hasRecipients(): boolean {
    return this._recipientList.length > 0;
  }
  
  private isValidEmail(email: string): boolean {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }
}

2๏ธโƒฃ Keep Getters Pure

class DataProcessor {
  private _data: number[] = [];
  private _processedCache: number[] | null = null;
  
  // โœ… Pure getter - no side effects
  get processed(): number[] {
    if (!this._processedCache) {
      this._processedCache = this._data.map(n => n * 2);
    }
    return [...this._processedCache];
  }
  
  // โœ… Setter invalidates cache appropriately
  set data(newData: number[]) {
    this._data = [...newData];
    this._processedCache = null;
  }
}

3๏ธโƒฃ Validate in Setters

class UserProfile {
  private _age: number = 0;
  private _email: string = "";
  private _username: string = "";
  
  // โœ… Comprehensive validation
  set age(value: number) {
    if (!Number.isInteger(value)) {
      throw new Error("๐Ÿšซ Age must be an integer");
    }
    if (value < 0 || value > 150) {
      throw new Error("๐Ÿšซ Age must be between 0 and 150");
    }
    this._age = value;
  }
  
  get age(): number {
    return this._age;
  }
  
  set email(value: string) {
    const trimmed = value.trim().toLowerCase();
    if (!this.isValidEmail(trimmed)) {
      throw new Error("๐Ÿšซ Invalid email format");
    }
    this._email = trimmed;
  }
  
  get email(): string {
    return this._email;
  }
  
  set username(value: string) {
    const trimmed = value.trim();
    if (trimmed.length < 3) {
      throw new Error("๐Ÿšซ Username must be at least 3 characters");
    }
    if (!/^[a-zA-Z0-9_]+$/.test(trimmed)) {
      throw new Error("๐Ÿšซ Username can only contain letters, numbers, and underscores");
    }
    this._username = trimmed;
  }
  
  get username(): string {
    return this._username;
  }
  
  private isValidEmail(email: string): boolean {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }
}

๐Ÿงช Hands-On Exercise

Time to practice! ๐ŸŽฏ Create a smart bank account system with getters and setters:

// Your challenge: Complete this implementation! ๐Ÿ’ช

class SmartBankAccount {
  private _balance: number = 0;
  private _overdraftLimit: number = 0;
  private _transactions: Array<{type: string, amount: number, date: Date}> = [];
  private _isLocked: boolean = false;
  
  constructor(
    public accountNumber: string,
    public accountHolder: string
  ) {}
  
  // TODO: Implement balance getter
  // Should return current balance
  
  // TODO: Implement balance setter
  // Should validate: no negative balance beyond overdraft limit
  // Should add transaction record
  // Should check if account is locked
  
  // TODO: Implement availableBalance getter
  // Should return balance + overdraft limit
  
  // TODO: Implement overdraftLimit setter
  // Should validate: must be >= 0
  // Should check if new limit would make current balance invalid
  
  // TODO: Implement isOverdrawn getter
  // Should return true if balance is negative
  
  // TODO: Implement lastTransaction getter
  // Should return the most recent transaction or null
  
  // TODO: Implement monthlyStatement getter
  // Should return formatted statement for current month
  
  // TODO: Add deposit(amount) and withdraw(amount) methods
  // These should use the balance setter internally
}

// Test your implementation! ๐Ÿงช
const account = new SmartBankAccount("ACC001", "John Doe");
account.overdraftLimit = 500;
account.deposit(1000);
account.withdraw(1200);
console.log(account.monthlyStatement);
๐Ÿ’ก Solution (click to reveal)
class SmartBankAccount {
  private _balance: number = 0;
  private _overdraftLimit: number = 0;
  private _transactions: Array<{type: string, amount: number, date: Date, balance: number}> = [];
  private _isLocked: boolean = false;
  
  constructor(
    public accountNumber: string,
    public accountHolder: string
  ) {}
  
  get balance(): number {
    return this._balance;
  }
  
  private set balance(amount: number) {
    if (this._isLocked) {
      throw new Error("๐Ÿ”’ Account is locked!");
    }
    
    const minAllowedBalance = -this._overdraftLimit;
    if (amount < minAllowedBalance) {
      throw new Error(`๐Ÿšซ Insufficient funds! Minimum allowed balance: $${minAllowedBalance}`);
    }
    
    this._balance = amount;
  }
  
  get availableBalance(): number {
    return this._balance + this._overdraftLimit;
  }
  
  set overdraftLimit(limit: number) {
    if (limit < 0) {
      throw new Error("๐Ÿšซ Overdraft limit cannot be negative!");
    }
    
    // Check if reducing limit would make current balance invalid
    const newMinBalance = -limit;
    if (this._balance < newMinBalance) {
      throw new Error("๐Ÿšซ Cannot reduce overdraft limit - account would be overdrawn!");
    }
    
    this._overdraftLimit = limit;
    console.log(`๐Ÿ’ณ Overdraft limit set to $${limit}`);
  }
  
  get overdraftLimit(): number {
    return this._overdraftLimit;
  }
  
  get isOverdrawn(): boolean {
    return this._balance < 0;
  }
  
  get lastTransaction(): {type: string, amount: number, date: Date, balance: number} | null {
    return this._transactions.length > 0 
      ? this._transactions[this._transactions.length - 1]
      : null;
  }
  
  get monthlyStatement(): string {
    const now = new Date();
    const currentMonth = now.getMonth();
    const currentYear = now.getFullYear();
    
    const monthlyTransactions = this._transactions.filter(t => {
      return t.date.getMonth() === currentMonth && 
             t.date.getFullYear() === currentYear;
    });
    
    let statement = `
๐Ÿฆ Monthly Statement
Account: ${this.accountNumber}
Holder: ${this.accountHolder}
Period: ${now.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

`;
    
    if (monthlyTransactions.length === 0) {
      statement += "No transactions this month\n";
    } else {
      monthlyTransactions.forEach(t => {
        const sign = t.type === 'deposit' ? '+' : '-';
        statement += `${t.date.toLocaleDateString()} | ${t.type.padEnd(10)} | ${sign}$${t.amount.toFixed(2).padStart(10)} | Balance: $${t.balance.toFixed(2)}\n`;
      });
    }
    
    statement += `
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Current Balance: $${this._balance.toFixed(2)}
Available Balance: $${this.availableBalance.toFixed(2)}
${this.isOverdrawn ? 'โš ๏ธ Account is overdrawn!' : 'โœ… Account in good standing'}
    `;
    
    return statement;
  }
  
  deposit(amount: number): void {
    if (amount <= 0) {
      throw new Error("๐Ÿšซ Deposit amount must be positive!");
    }
    
    const newBalance = this._balance + amount;
    this.balance = newBalance; // Uses setter validation
    
    this._transactions.push({
      type: 'deposit',
      amount,
      date: new Date(),
      balance: this._balance
    });
    
    console.log(`๐Ÿ’ฐ Deposited $${amount.toFixed(2)}. New balance: $${this._balance.toFixed(2)}`);
  }
  
  withdraw(amount: number): void {
    if (amount <= 0) {
      throw new Error("๐Ÿšซ Withdrawal amount must be positive!");
    }
    
    const newBalance = this._balance - amount;
    this.balance = newBalance; // Uses setter validation
    
    this._transactions.push({
      type: 'withdrawal',
      amount,
      date: new Date(),
      balance: this._balance
    });
    
    console.log(`๐Ÿ’ธ Withdrew $${amount.toFixed(2)}. New balance: $${this._balance.toFixed(2)}`);
  }
  
  lock(): void {
    this._isLocked = true;
    console.log("๐Ÿ”’ Account locked");
  }
  
  unlock(): void {
    this._isLocked = false;
    console.log("๐Ÿ”“ Account unlocked");
  }
}

๐ŸŽ“ Key Takeaways

Youโ€™ve mastered getters and setters in TypeScript! Hereโ€™s what youโ€™ve learned:

  1. Getters ๐Ÿ“ค:

    • Create computed properties
    • Provide read-only access
    • Enable lazy initialization
    • Keep them pure (no side effects)
  2. Setters ๐Ÿ“ฅ:

    • Validate input data
    • Transform values before storing
    • Trigger side effects
    • Maintain data integrity
  3. Best Practices ๐ŸŒŸ:

    • Use backing fields to avoid recursion
    • Validate in setters, compute in getters
    • Keep getters pure and predictable
    • Use descriptive names
  4. Common Use Cases ๐ŸŽฏ:

    • Form validation
    • Computed properties
    • Data transformation
    • Access control

๐Ÿค Next Steps

Congratulations on mastering getters and setters! ๐ŸŽ‰ Youโ€™re now equipped to create smart, self-validating objects that maintain their own integrity. Hereโ€™s what to explore next:

  • ๐Ÿ”„ Method Chaining: Create fluent interfaces
  • ๐ŸŽญ Abstract Classes: Design blueprint classes
  • ๐Ÿงฌ Inheritance: Extend classes effectively
  • ๐ŸŽจ Decorators: Add metadata to your classes

Remember: Getters and setters are your tools for creating intelligent objects that protect themselves and provide a clean API! ๐Ÿ›ก๏ธ

Happy coding! ๐Ÿš€โœจ