Dev Logs
/TypeScript/ Classes and Object-Oriented Programming
Chapters
  • 01Introduction to TypeScript
  • 02TypeScript Setup and Configuration
  • 03Basic Type Annotations
  • 04Interfaces and Type Aliases
  • 05Functions and Type Safety
  • 06Arrays, Tuples, and Enums
  • 07Union and Intersection Types
  • 08Type Inference and Narrowing
  • 09Optional and Readonly Properties
  • 10Classes and Object-Oriented Programming
    • Basic Classes
    • Class Declaration and Constructor
    • Property Initialization
    • Access Modifiers
    • Public, Private, and Protected
    • Private Fields (ES2022)
    • Getters and Setters
    • Property Accessors
    • Read-only Properties
    • Inheritance
    • Basic Inheritance with extends
    • Method Overriding and super
    • Static Members
    • Static Properties and Methods
    • Static Factory Methods
    • Abstract Classes
    • Abstract Classes and Methods
    • Implementing Interfaces
    • Classes Implementing Interfaces
    • Practical Examples
    • Event System
    • Repository Pattern
    • Best Practices
    • Good Practices
    • Avoid
    • Summary Checklist
    • Next Steps
  • 11Generics and Reusable Code
  • 12Type Guards and Advanced Type Checking
  • 13Advanced TypeScript Features
  • 14Modules and Namespaces
  • 15Error Handling in TypeScript
  • 16Utility Types and Type Manipulations
  • 17Declaration Merging and Ambient Declarations
All chapters

Classes and Object-Oriented Programming

Master TypeScript classes, access modifiers, inheritance, and object-oriented programming patterns

Basic Classes

Class Declaration and Constructor

typescript
// Basic class with constructor
class User {
  // Properties
  id: number;
  name: string;
  email: string;

  // Constructor
  constructor(id: number, name: string, email: string) {
    this.id = id;
    this.name = name;
    this.email = email;
  }

  // Methods
  getDisplayName(): string {
    return `${this.name} (${this.email})`;
  }

  updateEmail(newEmail: string): void {
    this.email = newEmail;
  }
}

// Creating instances
const user1 = new User(1, "John Doe", "john@example.com");
const user2 = new User(2, "Jane Smith", "jane@example.com");

console.log(user1.getDisplayName()); // "John Doe (john@example.com)"
user1.updateEmail("john.doe@example.com");

Property Initialization

typescript
// Property initialization in declaration
class Counter {
  count: number = 0; // Default value
  step: number = 1;

  constructor(initialCount?: number, step?: number) {
    if (initialCount !== undefined) {
      this.count = initialCount;
    }
    if (step !== undefined) {
      this.step = step;
    }
  }

  increment(): void {
    this.count += this.step;
  }

  decrement(): void {
    this.count -= this.step;
  }

  reset(): void {
    this.count = 0;
  }
}

// Shorthand constructor parameter properties
class Product {
  constructor(
    public id: number,
    public name: string,
    public price: number,
    public category: string = "general"
  ) {
    // Properties are automatically created and assigned
  }

  getFormattedPrice(): string {
    return `$${this.price.toFixed(2)}`;
  }
}

const product = new Product(1, "Laptop", 999.99, "electronics");
console.log(product.name); // "Laptop"
console.log(product.getFormattedPrice()); // "$999.99"

Access Modifiers

Public, Private, and Protected

typescript
class BankAccount {
  public accountNumber: string; // Accessible everywhere
  private balance: number; // Only accessible within this class
  protected accountType: string; // Accessible in this class and subclasses

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

  // Public method
  public getBalance(): number {
    return this.balance;
  }

  // Public method
  public deposit(amount: number): void {
    if (amount > 0) {
      this.balance += amount;
    }
  }

  // Public method
  public withdraw(amount: number): boolean {
    if (this.canWithdraw(amount)) {
      this.balance -= amount;
      return true;
    }
    return false;
  }

  // Private method - only accessible within this class
  private canWithdraw(amount: number): boolean {
    return amount > 0 && amount <= this.balance;
  }

  // Protected method - accessible in subclasses
  protected getAccountInfo(): string {
    return `${this.accountType} Account: ${this.accountNumber}`;
  }
}

const account = new BankAccount("123456789", 1000, "Checking");
console.log(account.accountNumber); // OK: public
console.log(account.getBalance()); // OK: public method
// console.log(account.balance); // Error: private
// account.canWithdraw(100); // Error: private method

Private Fields (ES2022)

typescript
// Using # for truly private fields (runtime private)
class SecureUser {
  #password: string; // Truly private
  #salt: string;
  public username: string;

  constructor(username: string, password: string) {
    this.username = username;
    this.#salt = this.generateSalt();
    this.#password = this.hashPassword(password);
  }

  #generateSalt(): string {
    return Math.random().toString(36).substring(2);
  }

  #hashPassword(password: string): string {
    return `${password}_${this.#salt}_hashed`;
  }

  public verifyPassword(password: string): boolean {
    return this.#hashPassword(password) === this.#password;
  }

  public changePassword(oldPassword: string, newPassword: string): boolean {
    if (this.verifyPassword(oldPassword)) {
      this.#password = this.hashPassword(newPassword);
      return true;
    }
    return false;
  }
}

const user = new SecureUser("john", "secret123");
console.log(user.username); // OK
// console.log(user.#password); // SyntaxError: Private field '#password' must be declared in an enclosing class

Getters and Setters

Property Accessors

typescript
class Temperature {
  private _celsius: number = 0;

  constructor(celsius: number) {
    this.celsius = celsius; // Uses setter for validation
  }

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

  // Setter with validation
  set celsius(value: number) {
    if (value < -273.15) {
      throw new Error("Temperature cannot be below absolute zero");
    }
    this._celsius = value;
  }

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

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

  get kelvin(): number {
    return this._celsius + 273.15;
  }

  set kelvin(value: number) {
    this.celsius = value - 273.15;
  }
}

const temp = new Temperature(25);
console.log(temp.celsius); // 25
console.log(temp.fahrenheit); // 77
console.log(temp.kelvin); // 298.15

temp.fahrenheit = 100;
console.log(temp.celsius); // 37.77777777777778

Read-only Properties

typescript
class ImmutablePoint {
  private _x: number;
  private _y: number;

  constructor(x: number, y: number) {
    this._x = x;
    this._y = y;
  }

  // Read-only getters
  get x(): number {
    return this._x;
  }

  get y(): number {
    return this._y;
  }

  // Computed properties
  get magnitude(): number {
    return Math.sqrt(this._x * this._x + this._y * this._y);
  }

  get angle(): number {
    return Math.atan2(this._y, this._x);
  }

  // Methods that return new instances (immutable pattern)
  translate(dx: number, dy: number): ImmutablePoint {
    return new ImmutablePoint(this._x + dx, this._y + dy);
  }

  scale(factor: number): ImmutablePoint {
    return new ImmutablePoint(this._x * factor, this._y * factor);
  }
}

const point = new ImmutablePoint(3, 4);
console.log(point.x, point.y); // 3, 4
console.log(point.magnitude); // 5
// point.x = 5; // Error: Cannot assign to 'x' because it is a read-only property

const newPoint = point.translate(1, 1);
console.log(newPoint.x, newPoint.y); // 4, 5

Inheritance

Basic Inheritance with extends

typescript
// Base class
class Animal {
  protected name: string;
  protected age: number;

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

  public makeSound(): string {
    return "Some generic animal sound";
  }

  public getInfo(): string {
    return `${this.name} is ${this.age} years old`;
  }

  protected sleep(): string {
    return `${this.name} is sleeping`;
  }
}

// Derived class
class Dog extends Animal {
  private breed: string;

  constructor(name: string, age: number, breed: string) {
    super(name, age); // Call parent constructor
    this.breed = breed;
  }

  // Override parent method
  public makeSound(): string {
    return "Woof! Woof!";
  }

  // Add new method
  public fetch(): string {
    return `${this.name} is fetching the ball`;
  }

  // Override and extend parent method
  public getInfo(): string {
    return `${super.getInfo()} and is a ${this.breed}`;
  }

  // Access protected method from parent
  public rest(): string {
    return this.sleep(); // Can access protected method
  }
}

class Cat extends Animal {
  private indoor: boolean;

  constructor(name: string, age: number, indoor: boolean = true) {
    super(name, age);
    this.indoor = indoor;
  }

  public makeSound(): string {
    return "Meow!";
  }

  public climb(): string {
    return `${this.name} is climbing`;
  }

  public getInfo(): string {
    const location = this.indoor ? "indoor" : "outdoor";
    return `${super.getInfo()} and is an ${location} cat`;
  }
}

// Usage
const dog = new Dog("Buddy", 3, "Golden Retriever");
const cat = new Cat("Whiskers", 2, true);

console.log(dog.makeSound()); // "Woof! Woof!"
console.log(cat.makeSound()); // "Meow!"
console.log(dog.getInfo()); // "Buddy is 3 years old and is a Golden Retriever"
console.log(cat.getInfo()); // "Whiskers is 2 years old and is an indoor cat"

Method Overriding and super

typescript
class Vehicle {
  protected brand: string;
  protected model: string;
  protected year: number;

  constructor(brand: string, model: string, year: number) {
    this.brand = brand;
    this.model = model;
    this.year = year;
  }

  public start(): string {
    return `Starting the ${this.brand} ${this.model}`;
  }

  public getDescription(): string {
    return `${this.year} ${this.brand} ${this.model}`;
  }

  protected performMaintenance(): string {
    return "Performing basic maintenance";
  }
}

class ElectricCar extends Vehicle {
  private batteryCapacity: number;
  private currentCharge: number;

  constructor(
    brand: string,
    model: string,
    year: number,
    batteryCapacity: number
  ) {
    super(brand, model, year);
    this.batteryCapacity = batteryCapacity;
    this.currentCharge = batteryCapacity; // Start fully charged
  }

  // Override start method
  public start(): string {
    if (this.currentCharge > 0) {
      return `${super.start()} - Electric motor engaged`;
    }
    return "Cannot start - battery depleted";
  }

  // Override and extend getDescription
  public getDescription(): string {
    return `${super.getDescription()} (Electric - ${this.batteryCapacity}kWh)`;
  }

  // New methods specific to electric cars
  public charge(amount: number): void {
    this.currentCharge = Math.min(
      this.currentCharge + amount,
      this.batteryCapacity
    );
  }

  public getBatteryLevel(): number {
    return (this.currentCharge / this.batteryCapacity) * 100;
  }

  // Override protected method
  protected performMaintenance(): string {
    return `${super.performMaintenance()} + Battery health check`;
  }

  public scheduleMaintenance(): string {
    return this.performMaintenance(); // Can access overridden protected method
  }
}

const tesla = new ElectricCar("Tesla", "Model 3", 2023, 75);
console.log(tesla.start()); // "Starting the Tesla Model 3 - Electric motor engaged"
console.log(tesla.getDescription()); // "2023 Tesla Model 3 (Electric - 75kWh)"
console.log(tesla.getBatteryLevel()); // 100

Static Members

Static Properties and Methods

typescript
class MathUtils {
  // Static properties
  static readonly PI = 3.14159;
  static readonly E = 2.71828;
  private static instanceCount = 0;

  // Instance property
  private id: number;

  constructor() {
    MathUtils.instanceCount++;
    this.id = MathUtils.instanceCount;
  }

  // Static methods
  static add(a: number, b: number): number {
    return a + b;
  }

  static multiply(a: number, b: number): number {
    return a * b;
  }

  static circleArea(radius: number): number {
    return MathUtils.PI * radius * radius;
  }

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

  // Instance method
  getId(): number {
    return this.id;
  }

  // Instance method accessing static member
  getCircleArea(radius: number): number {
    return MathUtils.circleArea(radius); // Access static method
  }
}

// Using static members without creating instances
console.log(MathUtils.PI); // 3.14159
console.log(MathUtils.add(5, 3)); // 8
console.log(MathUtils.circleArea(5)); // 78.5395

// Creating instances
const math1 = new MathUtils();
const math2 = new MathUtils();

console.log(math1.getId()); // 1
console.log(math2.getId()); // 2
console.log(MathUtils.getInstanceCount()); // 2

Static Factory Methods

typescript
class User {
  private constructor(
    public readonly id: string,
    public readonly name: string,
    public readonly email: string,
    public readonly role: "admin" | "user" | "guest"
  ) {}

  // Static factory methods
  static createAdmin(name: string, email: string): User {
    return new User(`admin_${Date.now()}`, name, email, "admin");
  }

  static createUser(name: string, email: string): User {
    return new User(`user_${Date.now()}`, name, email, "user");
  }

  static createGuest(): User {
    return new User(
      `guest_${Date.now()}`,
      "Guest",
      "guest@example.com",
      "guest"
    );
  }

  // Static validation method
  static isValidEmail(email: string): boolean {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }

  // Static factory with validation
  static fromData(data: {
    name: string;
    email: string;
    role?: "admin" | "user";
  }): User | null {
    if (!User.isValidEmail(data.email)) {
      return null;
    }

    const role = data.role || "user";
    return role === "admin"
      ? User.createAdmin(data.name, data.email)
      : User.createUser(data.name, data.email);
  }
}

// Usage
const admin = User.createAdmin("John Doe", "john@admin.com");
const user = User.createUser("Jane Smith", "jane@user.com");
const guest = User.createGuest();

const userFromData = User.fromData({
  name: "Bob Wilson",
  email: "bob@example.com",
  role: "user",
});

Abstract Classes

Abstract Classes and Methods

typescript
// Abstract base class
abstract class Shape {
  protected color: string;

  constructor(color: string) {
    this.color = color;
  }

  // Abstract method - must be implemented by subclasses
  abstract calculateArea(): number;
  abstract calculatePerimeter(): number;

  // Concrete method - can be used by subclasses
  getColor(): string {
    return this.color;
  }

  // Concrete method using abstract methods
  getDescription(): string {
    return `A ${this.color} shape with area ${this.calculateArea().toFixed(
      2
    )} and perimeter ${this.calculatePerimeter().toFixed(2)}`;
  }

  // Abstract method with default implementation
  display(): void {
    console.log(this.getDescription());
  }
}

// Concrete implementation
class Circle extends Shape {
  private radius: number;

  constructor(color: string, radius: number) {
    super(color);
    this.radius = radius;
  }

  // Must implement abstract methods
  calculateArea(): number {
    return Math.PI * this.radius * this.radius;
  }

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

  // Additional method specific to Circle
  getDiameter(): number {
    return this.radius * 2;
  }
}

class Rectangle extends Shape {
  private width: number;
  private height: number;

  constructor(color: string, width: number, height: number) {
    super(color);
    this.width = width;
    this.height = height;
  }

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

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

  // Override display method
  display(): void {
    console.log(`Rectangle: ${this.getDescription()}`);
  }
}

// Usage
const circle = new Circle("red", 5);
const rectangle = new Rectangle("blue", 4, 6);

circle.display(); // "A red shape with area 78.54 and perimeter 31.42"
rectangle.display(); // "Rectangle: A blue shape with area 24.00 and perimeter 20.00"

// Cannot instantiate abstract class
// const shape = new Shape("green"); // Error: Cannot create an instance of an abstract class

// Array of shapes
const shapes: Shape[] = [circle, rectangle];
shapes.forEach((shape) => shape.display());

Implementing Interfaces

Classes Implementing Interfaces

typescript
// Interface definitions
interface Flyable {
  fly(): string;
  altitude: number;
}

interface Swimmable {
  swim(): string;
  depth: number;
}

interface Walkable {
  walk(): string;
  speed: number;
}

// Class implementing single interface
class Bird implements Flyable {
  altitude: number = 0;

  constructor(private species: string) {}

  fly(): string {
    this.altitude = 100;
    return `${this.species} is flying at ${this.altitude} feet`;
  }

  land(): void {
    this.altitude = 0;
  }
}

// Class implementing multiple interfaces
class Duck implements Flyable, Swimmable, Walkable {
  altitude: number = 0;
  depth: number = 0;
  speed: number = 2;

  constructor(private name: string) {}

  fly(): string {
    this.altitude = 50;
    return `${this.name} is flying at ${this.altitude} feet`;
  }

  swim(): string {
    this.depth = 3;
    return `${this.name} is swimming at ${this.depth} feet deep`;
  }

  walk(): string {
    return `${this.name} is walking at ${this.speed} mph`;
  }

  // Additional methods
  quack(): string {
    return `${this.name} says quack!`;
  }
}

// Interface for class structure
interface Drawable {
  draw(): void;
  getArea(): number;
}

class Square implements Drawable {
  constructor(private sideLength: number) {}

  draw(): void {
    console.log(`Drawing a square with side length ${this.sideLength}`);
  }

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

// Usage
const bird = new Bird("Eagle");
const duck = new Duck("Donald");
const square = new Square(5);

console.log(bird.fly()); // "Eagle is flying at 100 feet"
console.log(duck.swim()); // "Donald is swimming at 3 feet deep"
console.log(duck.quack()); // "Donald says quack!"

square.draw(); // "Drawing a square with side length 5"
console.log(square.getArea()); // 25

Practical Examples

Event System

typescript
// Event system using classes
abstract class EventEmitter {
  private listeners: Map<string, Function[]> = new Map();

  on(event: string, listener: Function): void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, []);
    }
    this.listeners.get(event)!.push(listener);
  }

  off(event: string, listener: Function): void {
    const eventListeners = this.listeners.get(event);
    if (eventListeners) {
      const index = eventListeners.indexOf(listener);
      if (index > -1) {
        eventListeners.splice(index, 1);
      }
    }
  }

  protected emit(event: string, ...args: any[]): void {
    const eventListeners = this.listeners.get(event);
    if (eventListeners) {
      eventListeners.forEach((listener) => listener(...args));
    }
  }
}

class HttpClient extends EventEmitter {
  private baseUrl: string;

  constructor(baseUrl: string) {
    super();
    this.baseUrl = baseUrl;
  }

  async get(endpoint: string): Promise<any> {
    this.emit("request:start", { method: "GET", endpoint });

    try {
      const response = await fetch(`${this.baseUrl}${endpoint}`);
      const data = await response.json();

      this.emit("request:success", { method: "GET", endpoint, data });
      return data;
    } catch (error) {
      this.emit("request:error", { method: "GET", endpoint, error });
      throw error;
    }
  }
}

// Usage
const client = new HttpClient("https://api.example.com");

client.on("request:start", (details) => {
  console.log("Request started:", details);
});

client.on("request:success", (details) => {
  console.log("Request successful:", details);
});

client.on("request:error", (details) => {
  console.error("Request failed:", details);
});

Repository Pattern

typescript
// Repository pattern with classes
interface Repository<T> {
  findById(id: string): Promise<T | null>;
  findAll(): Promise<T[]>;
  create(entity: Omit<T, "id">): Promise<T>;
  update(id: string, updates: Partial<T>): Promise<T | null>;
  delete(id: string): Promise<boolean>;
}

abstract class BaseRepository<T extends { id: string }>
  implements Repository<T>
{
  protected items: Map<string, T> = new Map();

  async findById(id: string): Promise<T | null> {
    return this.items.get(id) || null;
  }

  async findAll(): Promise<T[]> {
    return Array.from(this.items.values());
  }

  async create(entity: Omit<T, "id">): Promise<T> {
    const id = this.generateId();
    const newEntity = { ...entity, id } as T;
    this.items.set(id, newEntity);
    return newEntity;
  }

  async update(id: string, updates: Partial<T>): Promise<T | null> {
    const existing = this.items.get(id);
    if (!existing) return null;

    const updated = { ...existing, ...updates };
    this.items.set(id, updated);
    return updated;
  }

  async delete(id: string): Promise<boolean> {
    return this.items.delete(id);
  }

  protected abstract generateId(): string;
}

interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
}

class UserRepository extends BaseRepository<User> {
  protected generateId(): string {
    return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }

  // Additional user-specific methods
  async findByEmail(email: string): Promise<User | null> {
    const users = await this.findAll();
    return users.find((user) => user.email === email) || null;
  }

  async findActiveUsers(): Promise<User[]> {
    const users = await this.findAll();
    const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
    return users.filter((user) => user.createdAt > thirtyDaysAgo);
  }
}

// Usage
const userRepo = new UserRepository();

async function example() {
  const user = await userRepo.create({
    name: "John Doe",
    email: "john@example.com",
    createdAt: new Date(),
  });

  console.log("Created user:", user);

  const foundUser = await userRepo.findByEmail("john@example.com");
  console.log("Found user:", foundUser);
}

Best Practices

✅ Good Practices

typescript
// Use access modifiers appropriately
class GoodExample {
  private _data: string[]; // Private implementation detail
  protected config: object; // For subclasses
  public readonly id: string; // Public immutable property

  constructor(id: string) {
    this.id = id;
    this._data = [];
    this.config = {};
  }

  // Public interface
  public addItem(item: string): void {
    this.validateItem(item);
    this._data.push(item);
  }

  // Private helper
  private validateItem(item: string): void {
    if (!item.trim()) {
      throw new Error("Item cannot be empty");
    }
  }
}

// Use composition over inheritance when appropriate
class Logger {
  log(message: string): void {
    console.log(`[${new Date().toISOString()}] ${message}`);
  }
}

class UserService {
  private logger = new Logger(); // Composition

  createUser(userData: any): User {
    this.logger.log("Creating user");
    // Implementation
    return new User(userData);
  }
}

// Use interfaces for contracts
interface PaymentProcessor {
  processPayment(amount: number): Promise<boolean>;
}

class StripeProcessor implements PaymentProcessor {
  async processPayment(amount: number): Promise<boolean> {
    // Stripe implementation
    return true;
  }
}

class PayPalProcessor implements PaymentProcessor {
  async processPayment(amount: number): Promise<boolean> {
    // PayPal implementation
    return true;
  }
}

❌ Avoid

typescript
// Don't make everything public
class BadExample {
  public internalData: any; // Should be private
  public helperMethod(): void {} // Should be private
}

// Don't use inheritance for code reuse only
class BadInheritance extends Array {
  // This is confusing - is it an array or something else?
}

// Don't ignore access modifiers
class IgnoredModifiers {
  private secret: string;

  constructor() {
    this.secret = "secret";
  }
}

// Accessing private members (bad practice)
const bad = new IgnoredModifiers();
// (bad as any).secret; // Don't do this!

Summary Checklist

  • Use appropriate access modifiers (public, private, protected)
  • Understand the difference between TypeScript private and # private fields
  • Use getters and setters for controlled property access
  • Implement inheritance with extends and super
  • Use abstract classes for shared behavior with required implementations
  • Implement interfaces to define contracts
  • Use static members for class-level functionality
  • Prefer composition over inheritance when appropriate
  • Use readonly for immutable properties
  • Follow the principle of least privilege for access modifiers

Next Steps

Now that you understand classes and OOP in TypeScript, let's explore generics for creating reusable and type-safe code.


Continue to: Generics and Reusable Code

PreviousOptional and Readonly PropertiesNextGenerics and Reusable Code

Open source, free forever. Built by iammhador.

Contribute on GitHub