You are viewing a preview of this lesson. Sign in to start learning
Back to TypeScript

TypeScript Classes

Build reusable blueprints for objects with properties, methods, and constructor functions.

TypeScript Classes

Master object-oriented programming in TypeScript with free flashcards and interactive examples. This lesson covers class syntax, constructors, access modifiers, inheritance, abstract classes, and static membersโ€”essential concepts for building scalable TypeScript applications.

Welcome to TypeScript Classes ๐Ÿ’ป

Classes are the backbone of object-oriented programming (OOP) in TypeScript. They provide a blueprint for creating objects with specific properties and behaviors, enabling you to write more organized, maintainable, and reusable code. Whether you're building a web application, mobile app, or backend service, understanding classes is crucial for leveraging TypeScript's full potential.

In this lesson, you'll learn how to define classes, work with constructors, control access to members, implement inheritance, and use advanced features like abstract classes and static members. By the end, you'll be equipped to design robust, type-safe class hierarchies that make your code easier to understand and extend.


Core Concepts: Understanding TypeScript Classes ๐Ÿ—๏ธ

What is a Class?

A class is a template for creating objects. It encapsulates data (properties) and functionality (methods) into a single unit. Think of a class as a cookie cutter: it defines the shape, but you need to use it to create actual cookies (objects).

Basic Class Syntax:

class Person {
  name: string;
  age: number;

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

  greet(): void {
    console.log(`Hello, my name is ${this.name}`);
  }
}

const alice = new Person("Alice", 30);
alice.greet(); // Output: Hello, my name is Alice

Key Components:

  • Properties (name, age): Store data for each instance
  • Constructor: Special method that initializes new instances
  • Methods (greet): Functions that define behaviors
  • Instances: Objects created from the class using new

๐Ÿ’ก Tip: Always use PascalCase for class names (e.g., UserAccount, ShoppingCart) to distinguish them from regular variables.


Constructors and Initialization โš™๏ธ

The constructor is a special method called when you create a new instance of a class. It's where you initialize properties and set up the object's initial state.

Constructor with Parameter Properties:

TypeScript offers a shorthand syntax that automatically creates and assigns properties:

class Product {
  constructor(
    public name: string,
    public price: number,
    private inventory: number
  ) {}

  isInStock(): boolean {
    return this.inventory > 0;
  }
}

const laptop = new Product("Laptop", 999, 5);
console.log(laptop.name); // "Laptop"
// console.log(laptop.inventory); // Error: Property 'inventory' is private

This shorthand automatically:

  1. Declares the properties
  2. Sets their types
  3. Assigns constructor arguments to properties
  4. Applies access modifiers

Access Modifiers: Controlling Visibility ๐Ÿ”’

TypeScript provides three access modifiers to control who can access class members:

ModifierClassSubclassesOutsideUse Case
publicโœ…โœ…โœ…Default - accessible everywhere
privateโœ…โŒโŒInternal implementation details
protectedโœ…โœ…โŒShared with subclasses only

Example:

class BankAccount {
  public accountNumber: string;
  private balance: number;
  protected ownerName: string;

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

  public deposit(amount: number): void {
    this.balance += amount;
  }

  public getBalance(): number {
    return this.balance; // OK: accessing private member within class
  }
}

class SavingsAccount extends BankAccount {
  addInterest(rate: number): void {
    // this.balance += this.balance * rate; // Error: balance is private
    console.log(this.ownerName); // OK: ownerName is protected
  }
}

const account = new BankAccount("123456", 1000, "John");
console.log(account.accountNumber); // OK
// console.log(account.balance); // Error: private
// console.log(account.ownerName); // Error: protected

๐Ÿง  Memory Device: Think of access modifiers like house security:

  • public: Front door (everyone welcome)
  • protected: Family room (family members only)
  • private: Personal bedroom (just you)


Inheritance: Building Class Hierarchies ๐ŸŒณ

Inheritance allows you to create a new class based on an existing class, inheriting its properties and methods. Use the extends keyword.

class Animal {
  constructor(public name: string) {}

  move(distance: number): void {
    console.log(`${this.name} moved ${distance} meters`);
  }
}

class Dog extends Animal {
  bark(): void {
    console.log("Woof! Woof!");
  }
}

class Bird extends Animal {
  fly(distance: number): void {
    console.log(`${this.name} flew ${distance} meters`);
    this.move(distance);
  }
}

const dog = new Dog("Buddy");
dog.move(10); // Inherited method
dog.bark();   // Dog-specific method

const bird = new Bird("Tweety");
bird.fly(50); // Bird-specific method

Overriding Methods with super:

You can override parent methods and call the parent implementation using super:

class Employee {
  constructor(public name: string, public salary: number) {}

  getDetails(): string {
    return `${this.name} earns $${this.salary}`;
  }
}

class Manager extends Employee {
  constructor(name: string, salary: number, public department: string) {
    super(name, salary); // Call parent constructor
  }

  getDetails(): string {
    return `${super.getDetails()} and manages ${this.department}`;
  }
}

const manager = new Manager("Sarah", 80000, "Engineering");
console.log(manager.getDetails());
// Output: Sarah earns $80000 and manages Engineering

โš ๏ธ Important: Always call super() in the child constructor before accessing this.


Abstract Classes: Defining Contracts ๐Ÿ“œ

Abstract classes serve as base classes that cannot be instantiated directly. They define a contract that subclasses must follow.

abstract class Shape {
  constructor(public color: string) {}

  abstract getArea(): number; // Must be implemented by subclasses
  abstract getPerimeter(): number;

  describe(): void {
    console.log(`A ${this.color} shape with area ${this.getArea()}`);
  }
}

class Circle extends Shape {
  constructor(color: string, public radius: number) {
    super(color);
  }

  getArea(): number {
    return Math.PI * this.radius ** 2;
  }

  getPerimeter(): number {
    return 2 * Math.PI * this.radius;
  }
}

class Rectangle extends Shape {
  constructor(color: string, public width: number, public height: number) {
    super(color);
  }

  getArea(): number {
    return this.width * this.height;
  }

  getPerimeter(): number {
    return 2 * (this.width + this.height);
  }
}

// const shape = new Shape("red"); // Error: Cannot create instance of abstract class
const circle = new Circle("blue", 5);
circle.describe(); // Output: A blue shape with area 78.54...

When to Use Abstract Classes:

  • When you want to provide default implementations for some methods
  • When subclasses share common functionality
  • When you need to enforce a contract with abstract methods


Static Members: Class-Level Properties and Methods ๐ŸŽฏ

Static members belong to the class itself, not to instances. They're useful for utility functions, constants, or tracking shared data.

class MathUtils {
  static PI: number = 3.14159;
  static instanceCount: number = 0;

  constructor() {
    MathUtils.instanceCount++;
  }

  static calculateCircleArea(radius: number): number {
    return this.PI * radius ** 2;
  }

  static getInstanceCount(): number {
    return MathUtils.instanceCount;
  }
}

// Access static members via class name
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.calculateCircleArea(5)); // 78.5398...

const util1 = new MathUtils();
const util2 = new MathUtils();
console.log(MathUtils.getInstanceCount()); // 2

Singleton Pattern with Static:

class DatabaseConnection {
  private static instance: DatabaseConnection;
  private constructor() {} // Private constructor prevents direct instantiation

  static getInstance(): DatabaseConnection {
    if (!DatabaseConnection.instance) {
      DatabaseConnection.instance = new DatabaseConnection();
    }
    return DatabaseConnection.instance;
  }

  connect(): void {
    console.log("Connected to database");
  }
}

const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();
console.log(db1 === db2); // true - same instance

Readonly Properties: Immutable Data ๐Ÿ”

The readonly modifier prevents properties from being changed after initialization:

class User {
  readonly id: string;
  name: string;

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

  updateName(newName: string): void {
    this.name = newName; // OK
    // this.id = "new-id"; // Error: Cannot assign to 'id' because it is read-only
  }
}

const user = new User("u123", "Alice");
user.name = "Alicia"; // OK
// user.id = "u456"; // Error: read-only

๐Ÿ’ก Tip: Use readonly for properties that should never change after object creation, like IDs, timestamps, or configuration values.


Getters and Setters: Controlled Access ๐ŸŽ›๏ธ

Getters and setters provide computed properties and validation:

class Temperature {
  private _celsius: number = 0;

  get celsius(): number {
    return this._celsius;
  }

  set celsius(value: number) {
    if (value < -273.15) {
      throw new Error("Temperature below absolute zero!");
    }
    this._celsius = value;
  }

  get fahrenheit(): number {
    return (this._celsius * 9/5) + 32;
  }

  set fahrenheit(value: number) {
    this.celsius = (value - 32) * 5/9;
  }
}

const temp = new Temperature();
temp.celsius = 25; // Uses setter
console.log(temp.fahrenheit); // Uses getter: 77

temp.fahrenheit = 32;
console.log(temp.celsius); // 0

Example 1: Building a Library System ๐Ÿ“š

Let's create a library management system demonstrating multiple class concepts:

abstract class LibraryItem {
  constructor(
    public readonly id: string,
    public title: string,
    protected availableCopies: number
  ) {}

  abstract getItemType(): string;

  checkout(): boolean {
    if (this.availableCopies > 0) {
      this.availableCopies--;
      return true;
    }
    return false;
  }

  returnItem(): void {
    this.availableCopies++;
  }

  isAvailable(): boolean {
    return this.availableCopies > 0;
  }
}

class Book extends LibraryItem {
  constructor(
    id: string,
    title: string,
    availableCopies: number,
    public author: string,
    public isbn: string
  ) {
    super(id, title, availableCopies);
  }

  getItemType(): string {
    return "Book";
  }

  getInfo(): string {
    return `"${this.title}" by ${this.author} (ISBN: ${this.isbn})`;
  }
}

class DVD extends LibraryItem {
  constructor(
    id: string,
    title: string,
    availableCopies: number,
    public director: string,
    public duration: number
  ) {
    super(id, title, availableCopies);
  }

  getItemType(): string {
    return "DVD";
  }

  getInfo(): string {
    return `"${this.title}" directed by ${this.director} (${this.duration} min)`;
  }
}

class Library {
  private static nextId: number = 1;
  private items: LibraryItem[] = [];

  static generateId(): string {
    return `LIB${String(this.nextId++).padStart(5, '0')}`;
  }

  addItem(item: LibraryItem): void {
    this.items.push(item);
  }

  findById(id: string): LibraryItem | undefined {
    return this.items.find(item => item.id === id);
  }

  getAvailableItems(): LibraryItem[] {
    return this.items.filter(item => item.isAvailable());
  }
}

// Usage
const library = new Library();

const book = new Book(
  Library.generateId(),
  "TypeScript Handbook",
  3,
  "Microsoft",
  "978-0000000000"
);

const dvd = new DVD(
  Library.generateId(),
  "The Matrix",
  2,
  "Wachowskis",
  136
);

library.addItem(book);
library.addItem(dvd);

console.log(book.checkout()); // true
console.log(book.getInfo());
console.log(library.getAvailableItems().length);

Key Concepts Demonstrated:

  • Abstract base class (LibraryItem) defining common behavior
  • Inheritance with Book and DVD extending base class
  • Protected properties accessible in subclasses
  • Static method for ID generation
  • Public, private, and protected access control
  • Readonly property (id) that cannot be modified


Example 2: E-Commerce Shopping Cart ๐Ÿ›’

A practical example showing class interaction and encapsulation:

class Product {
  constructor(
    public readonly sku: string,
    public name: string,
    public price: number,
    private stock: number
  ) {}

  isInStock(quantity: number = 1): boolean {
    return this.stock >= quantity;
  }

  reduceStock(quantity: number): void {
    if (!this.isInStock(quantity)) {
      throw new Error(`Insufficient stock for ${this.name}`);
    }
    this.stock -= quantity;
  }

  get stockLevel(): number {
    return this.stock;
  }
}

class CartItem {
  constructor(
    public product: Product,
    private _quantity: number
  ) {
    if (_quantity <= 0) {
      throw new Error("Quantity must be positive");
    }
  }

  get quantity(): number {
    return this._quantity;
  }

  set quantity(value: number) {
    if (value <= 0) {
      throw new Error("Quantity must be positive");
    }
    if (!this.product.isInStock(value)) {
      throw new Error("Not enough stock");
    }
    this._quantity = value;
  }

  get subtotal(): number {
    return this.product.price * this._quantity;
  }
}

class ShoppingCart {
  private items: CartItem[] = [];

  addItem(product: Product, quantity: number = 1): void {
    const existingItem = this.items.find(item => item.product.sku === product.sku);

    if (existingItem) {
      existingItem.quantity += quantity;
    } else {
      this.items.push(new CartItem(product, quantity));
    }
  }

  removeItem(sku: string): void {
    this.items = this.items.filter(item => item.product.sku !== sku);
  }

  get total(): number {
    return this.items.reduce((sum, item) => sum + item.subtotal, 0);
  }

  get itemCount(): number {
    return this.items.reduce((sum, item) => sum + item.quantity, 0);
  }

  checkout(): void {
    for (const item of this.items) {
      item.product.reduceStock(item.quantity);
    }
    this.items = [];
  }
}

// Usage
const laptop = new Product("LAP001", "Gaming Laptop", 1299.99, 5);
const mouse = new Product("MOU001", "Wireless Mouse", 29.99, 20);

const cart = new ShoppingCart();
cart.addItem(laptop, 1);
cart.addItem(mouse, 2);

console.log(`Total items: ${cart.itemCount}`);
console.log(`Total price: $${cart.total.toFixed(2)}`);

cart.checkout();
console.log(`Laptop stock after checkout: ${laptop.stockLevel}`);

Design Patterns Used:

  • Encapsulation: Private properties with controlled access via methods/getters
  • Validation: Setters enforce business rules (positive quantities, stock checks)
  • Composition: CartItem contains Product, ShoppingCart contains CartItem[]
  • Computed Properties: subtotal, total, and itemCount are calculated on-demand

Example 3: Game Character System ๐ŸŽฎ

Demonstrating inheritance hierarchy and polymorphism:

abstract class Character {
  protected health: number;
  protected maxHealth: number;

  constructor(
    public name: string,
    maxHealth: number,
    protected attackPower: number
  ) {
    this.maxHealth = maxHealth;
    this.health = maxHealth;
  }

  abstract attack(target: Character): void;
  abstract getCharacterType(): string;

  takeDamage(damage: number): void {
    this.health = Math.max(0, this.health - damage);
    console.log(`${this.name} took ${damage} damage. Health: ${this.health}/${this.maxHealth}`);
  }

  isAlive(): boolean {
    return this.health > 0;
  }

  heal(amount: number): void {
    this.health = Math.min(this.maxHealth, this.health + amount);
    console.log(`${this.name} healed ${amount}. Health: ${this.health}/${this.maxHealth}`);
  }

  get healthPercentage(): number {
    return (this.health / this.maxHealth) * 100;
  }
}

class Warrior extends Character {
  private rage: number = 0;

  constructor(name: string) {
    super(name, 150, 20);
  }

  attack(target: Character): void {
    const damage = this.attackPower + this.rage;
    console.log(`${this.name} attacks ${target.name} for ${damage} damage!`);
    target.takeDamage(damage);
    this.rage = Math.min(50, this.rage + 5); // Build rage
  }

  getCharacterType(): string {
    return "Warrior";
  }

  specialAbility(): void {
    console.log(`${this.name} uses Whirlwind! Rage: ${this.rage}`);
    this.rage = 0;
  }
}

class Mage extends Character {
  private mana: number = 100;

  constructor(name: string) {
    super(name, 80, 15);
  }

  attack(target: Character): void {
    if (this.mana >= 20) {
      const damage = this.attackPower * 2;
      console.log(`${this.name} casts Fireball at ${target.name} for ${damage} damage!`);
      target.takeDamage(damage);
      this.mana -= 20;
    } else {
      console.log(`${this.name} is out of mana!`);
    }
  }

  getCharacterType(): string {
    return "Mage";
  }

  restoreMana(amount: number): void {
    this.mana = Math.min(100, this.mana + amount);
  }
}

class Healer extends Character {
  constructor(name: string) {
    super(name, 100, 10);
  }

  attack(target: Character): void {
    console.log(`${this.name} attacks ${target.name} for ${this.attackPower} damage!`);
    target.takeDamage(this.attackPower);
  }

  getCharacterType(): string {
    return "Healer";
  }

  healAlly(ally: Character): void {
    const healAmount = 30;
    console.log(`${this.name} heals ${ally.name} for ${healAmount}!`);
    ally.heal(healAmount);
  }
}

// Battle simulation
const warrior = new Warrior("Thorin");
const mage = new Mage("Gandalf");
const healer = new Healer("Elara");

warrior.attack(mage);
mage.attack(warrior);
healer.healAlly(warrior);

console.log(`${warrior.name} health: ${warrior.healthPercentage.toFixed(1)}%`);

Object-Oriented Principles:

  • Polymorphism: Different implementations of attack() in each subclass
  • Abstraction: Character provides interface without implementation details
  • Inheritance: Shared functionality in base class, specialized in subclasses
  • Encapsulation: Protected properties prevent direct external manipulation

Example 4: Account Management with Interfaces ๐Ÿฆ

Combining classes with TypeScript interfaces:

interface Auditable {
  getAuditLog(): string[];
  logAction(action: string): void;
}

interface Transferable {
  transfer(amount: number, to: Account): boolean;
}

abstract class Account implements Auditable {
  protected balance: number;
  protected auditLog: string[] = [];

  constructor(
    public readonly accountId: string,
    protected accountHolder: string,
    initialBalance: number
  ) {
    this.balance = initialBalance;
    this.logAction(`Account created with balance: ${initialBalance}`);
  }

  abstract getAccountType(): string;

  deposit(amount: number): void {
    if (amount <= 0) {
      throw new Error("Deposit amount must be positive");
    }
    this.balance += amount;
    this.logAction(`Deposited: ${amount}`);
  }

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

  logAction(action: string): void {
    const timestamp = new Date().toISOString();
    this.auditLog.push(`[${timestamp}] ${action}`);
  }

  getAuditLog(): string[] {
    return [...this.auditLog]; // Return copy
  }
}

class CheckingAccount extends Account implements Transferable {
  private static TRANSFER_FEE = 2.50;

  constructor(accountId: string, accountHolder: string, initialBalance: number) {
    super(accountId, accountHolder, initialBalance);
  }

  getAccountType(): string {
    return "Checking";
  }

  withdraw(amount: number): boolean {
    if (amount > this.balance) {
      this.logAction(`Failed withdrawal: ${amount} (insufficient funds)`);
      return false;
    }
    this.balance -= amount;
    this.logAction(`Withdrew: ${amount}`);
    return true;
  }

  transfer(amount: number, to: Account): boolean {
    const totalAmount = amount + CheckingAccount.TRANSFER_FEE;
    if (totalAmount > this.balance) {
      this.logAction(`Failed transfer: ${amount} to ${to.accountId}`);
      return false;
    }
    this.balance -= totalAmount;
    to.deposit(amount);
    this.logAction(`Transferred: ${amount} to ${to.accountId} (Fee: ${CheckingAccount.TRANSFER_FEE})`);
    return true;
  }
}

class SavingsAccount extends Account {
  constructor(
    accountId: string,
    accountHolder: string,
    initialBalance: number,
    private interestRate: number
  ) {
    super(accountId, accountHolder, initialBalance);
  }

  getAccountType(): string {
    return "Savings";
  }

  applyInterest(): void {
    const interest = this.balance * (this.interestRate / 100);
    this.balance += interest;
    this.logAction(`Interest applied: ${interest.toFixed(2)}`);
  }
}

// Usage
const checking = new CheckingAccount("CHK001", "Alice Smith", 1000);
const savings = new SavingsAccount("SAV001", "Bob Johnson", 5000, 2.5);

checking.deposit(500);
checking.transfer(200, savings);
savings.applyInterest();

console.log(`Checking balance: $${checking.getBalance()}`);
console.log(`Savings balance: $${savings.getBalance()}`);
console.log("Audit log:", checking.getAuditLog());

Key Takeaways from This Example:

  • Interfaces define contracts that classes must implement
  • Multiple interfaces can be implemented by a single class
  • Static properties (TRANSFER_FEE) are shared across all instances
  • Type safety ensures only accounts with Transferable can call transfer()

Common Mistakes to Avoid โš ๏ธ

1. Forgetting to Call super() in Child Constructor

โŒ Wrong:

class Employee extends Person {
  constructor(name: string, public employeeId: string) {
    // Missing super()
    this.name = name; // Error: Must call super first
  }
}

โœ… Correct:

class Employee extends Person {
  constructor(name: string, public employeeId: string) {
    super(name); // Call parent constructor first
    // Now can use 'this'
  }
}
2. Accessing Private Members from Outside

โŒ Wrong:

class User {
  private password: string;
  
  constructor(password: string) {
    this.password = password;
  }
}

const user = new User("secret123");
console.log(user.password); // Error: 'password' is private

โœ… Correct:

class User {
  private password: string;
  
  constructor(password: string) {
    this.password = password;
  }
  
  verifyPassword(input: string): boolean {
    return this.password === input;
  }
}
3. Modifying Readonly Properties

โŒ Wrong:

class Order {
  constructor(public readonly orderId: string) {}
  
  updateId(newId: string): void {
    this.orderId = newId; // Error: Cannot assign to readonly
  }
}

โœ… Correct:

class Order {
  constructor(public readonly orderId: string) {}
  
  // Readonly properties cannot be changed after construction
  // Create a new instance if you need different ID
}
4. Not Implementing All Abstract Methods

โŒ Wrong:

abstract class Vehicle {
  abstract startEngine(): void;
  abstract stopEngine(): void;
}

class Car extends Vehicle {
  startEngine(): void {
    console.log("Engine started");
  }
  // Missing stopEngine() - Error!
}

โœ… Correct:

class Car extends Vehicle {
  startEngine(): void {
    console.log("Engine started");
  }
  
  stopEngine(): void {
    console.log("Engine stopped");
  }
}
5. Confusing Static and Instance Members

โŒ Wrong:

class Counter {
  static count: number = 0;
  
  increment(): void {
    this.count++; // Error: Static accessed via 'this'
  }
}

โœ… Correct:

class Counter {
  static count: number = 0;
  
  increment(): void {
    Counter.count++; // Use class name for static
  }
}
6. Returning Wrong Types from Getters

โŒ Wrong:

class Product {
  private _price: number = 0;
  
  get price(): number {
    return `$${this._price}`; // Error: string not assignable to number
  }
}

โœ… Correct:

class Product {
  private _price: number = 0;
  
  get price(): number {
    return this._price;
  }
  
  get formattedPrice(): string {
    return `$${this._price.toFixed(2)}`;
  }
}

Key Takeaways ๐ŸŽฏ

๐Ÿ“‹ TypeScript Classes Quick Reference

ConceptSyntaxPurpose
Basic Classclass Name { }Define object blueprint
Constructorconstructor(params) { }Initialize instances
Publicpublic propAccessible everywhere (default)
Privateprivate propOnly within class
Protectedprotected propClass + subclasses
Readonlyreadonly propCannot change after init
Staticstatic propBelongs to class, not instance
Inheritanceclass B extends AInherit from parent class
Supersuper(args)Call parent constructor/method
Abstract Classabstract classCannot instantiate directly
Abstract Methodabstract method()Must implement in subclass
Getterget prop() { }Computed property (read)
Setterset prop(v) { }Controlled assignment (write)

Essential Rules:

  1. โœ… Always call super() before using this in child constructors
  2. โœ… Use access modifiers to control visibility and protect data
  3. โœ… Make properties readonly when they shouldn't change
  4. โœ… Implement all abstract methods in concrete subclasses
  5. โœ… Access static members via class name, not this
  6. โœ… Use getters/setters for computed or validated properties
  7. โœ… Favor composition over deep inheritance hierarchies

Best Practices:

  • ๐ŸŽฏ Single Responsibility: Each class should have one clear purpose
  • ๐Ÿ”’ Encapsulation: Keep implementation details private
  • ๐Ÿ—๏ธ Composition over Inheritance: Use "has-a" relationships when possible
  • ๐Ÿ“ Meaningful Names: Use descriptive class and method names
  • โšก Avoid God Classes: Don't make one class do everything

๐Ÿ“š Further Study

  1. TypeScript Official Handbook - Classes: https://www.typescriptlang.org/docs/handbook/2/classes.html - Comprehensive guide to TypeScript class features with examples and best practices

  2. MDN Web Docs - Classes: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes - Understanding JavaScript classes that TypeScript builds upon

  3. Refactoring Guru - Design Patterns: https://refactoring.guru/design-patterns/typescript - Learn common OOP design patterns implemented in TypeScript


๐ŸŽ‰ Congratulations! You now understand TypeScript classes, from basic syntax to advanced concepts like abstract classes and static members. Practice building class hierarchies for real-world scenarios to reinforce these concepts. Remember: good class design makes your code more maintainable, testable, and scalable!