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:
-
Getters ๐ค:
- Create computed properties
- Provide read-only access
- Enable lazy initialization
- Keep them pure (no side effects)
-
Setters ๐ฅ:
- Validate input data
- Transform values before storing
- Trigger side effects
- Maintain data integrity
-
Best Practices ๐:
- Use backing fields to avoid recursion
- Validate in setters, compute in getters
- Keep getters pure and predictable
- Use descriptive names
-
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! ๐โจ