Skip to content

Classes#

TypeScript adds powerful type checking to JavaScript classes, along with features like access modifiers, abstract classes, and interfaces.

Basic Class Syntax#

class Person {
  // Properties
  name: string;
  age: number;

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

  // Method
  greet(): string {
    return `Hello, I'm ${this.name} and I'm ${this.age} years old.`;
  }
}

const person = new Person("Alice", 25);
console.log(person.greet());

Property Initialization#

// Traditional initialization
class User {
  id: number;
  name: string;

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

// Shorthand with parameter properties
class User2 {
  constructor(
    public id: number,
    public name: string
  ) {}
}

// Both classes work the same way
const user1 = new User(1, "Alice");
const user2 = new User2(1, "Alice");

Access Modifiers#

Public (default)#

class Car {
  public brand: string;

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

  public drive(): void {
    console.log(`Driving ${this.brand}`);
  }
}

const car = new Car("Toyota");
console.log(car.brand);  // ✅ Accessible
car.drive();             // ✅ Accessible

Private#

class BankAccount {
  private balance: number = 0;

  constructor(initialBalance: number) {
    this.balance = initialBalance;
  }

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

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

const account = new BankAccount(1000);
account.deposit(500);
console.log(account.getBalance());  // ✅ 1500
// console.log(account.balance);    // ❌ Error: Property 'balance' is private

Protected#

class Animal {
  protected name: string;

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

class Dog extends Animal {
  private breed: string;

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

  public getInfo(): string {
    return `${this.name} is a ${this.breed}`; // ✅ Can access protected property
  }
}

const dog = new Dog("Buddy", "Labrador");
console.log(dog.getInfo());  // ✅ "Buddy is a Labrador"
// console.log(dog.name);    // ❌ Error: Property 'name' is protected

Readonly Properties#

class Configuration {
  readonly apiKey: string;
  readonly apiUrl: string;
  timeout: number;

  constructor(apiKey: string, apiUrl: string, timeout: number) {
    this.apiKey = apiKey;
    this.apiUrl = apiUrl;
    this.timeout = timeout;
  }

  updateTimeout(newTimeout: number): void {
    this.timeout = newTimeout;           // ✅ OK
    // this.apiKey = "new-key";          // ❌ Error: Cannot assign to 'apiKey'
  }
}

Getters and Setters#

class User {
  private _email: string = "";

  get email(): string {
    return this._email;
  }

  set email(value: string) {
    if (!value.includes("@")) {
      throw new Error("Invalid email format");
    }
    this._email = value;
  }

  // Computed property
  private _firstName: string = "";
  private _lastName: string = "";

  get fullName(): string {
    return `${this._firstName} ${this._lastName}`;
  }

  set fullName(value: string) {
    const [first, last] = value.split(" ");
    this._firstName = first;
    this._lastName = last;
  }
}

const user = new User();
user.email = "user@example.com";  // ✅ OK
console.log(user.email);
// user.email = "invalid";        // ❌ Throws error

user.fullName = "John Doe";
console.log(user.fullName);       // "John Doe"

Static Members#

class MathUtils {
  static PI: number = 3.14159;

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

  static max(...numbers: number[]): number {
    return Math.max(...numbers);
  }
}

// Access without instantiation
console.log(MathUtils.PI);                    // 3.14159
console.log(MathUtils.calculateCircleArea(5)); // 78.53975
console.log(MathUtils.max(1, 5, 3, 9, 2));    // 9

Inheritance#

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

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

class Dog extends Animal {
  constructor(name: string, public breed: string) {
    super(name);  // Call parent constructor
  }

  bark(): void {
    console.log("Woof! Woof!");
  }

  // Override parent method
  move(distance: number): void {
    console.log("Running...");
    super.move(distance);  // Call parent method
  }
}

const dog = new Dog("Buddy", "Labrador");
dog.bark();       // "Woof! Woof!"
dog.move(10);     // "Running..." then "Buddy moved 10 meters."

Abstract Classes#

abstract class Shape {
  abstract getArea(): number;
  abstract getPerimeter(): number;

  // Concrete method
  describe(): string {
    return `Area: ${this.getArea()}, Perimeter: ${this.getPerimeter()}`;
  }
}

class Circle extends Shape {
  constructor(private radius: number) {
    super();
  }

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

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

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

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

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

const circle = new Circle(5);
console.log(circle.describe());

const rectangle = new Rectangle(4, 6);
console.log(rectangle.describe());

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

Implementing Interfaces#

interface Printable {
  print(): void;
}

interface Saveable {
  save(): Promise<void>;
}

class Document implements Printable, Saveable {
  constructor(private content: string) {}

  print(): void {
    console.log(this.content);
  }

  async save(): Promise<void> {
    // Simulate async save
    await new Promise(resolve => setTimeout(resolve, 1000));
    console.log("Document saved");
  }
}

const doc = new Document("Hello, World!");
doc.print();
await doc.save();

Generic Classes#

class Box<T> {
  private item: T | null = null;

  setItem(item: T): void {
    this.item = item;
  }

  getItem(): T | null {
    return this.item;
  }

  hasItem(): boolean {
    return this.item !== null;
  }
}

const numberBox = new Box<number>();
numberBox.setItem(42);
console.log(numberBox.getItem()); // 42

const stringBox = new Box<string>();
stringBox.setItem("Hello");
console.log(stringBox.getItem()); // "Hello"

// Generic class with constraints
class Repository<T extends { id: number }> {
  private items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

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

  getAll(): T[] {
    return this.items;
  }
}

interface User {
  id: number;
  name: string;
}

const userRepo = new Repository<User>();
userRepo.add({ id: 1, name: "Alice" });
userRepo.add({ id: 2, name: "Bob" });
console.log(userRepo.findById(1));

Method Overloading#

class Calculator {
  // Overload signatures
  add(a: number, b: number): number;
  add(a: string, b: string): string;
  add(a: number[], b: number[]): number[];

  // Implementation
  add(a: any, b: any): any {
    if (typeof a === "number" && typeof b === "number") {
      return a + b;
    }
    if (typeof a === "string" && typeof b === "string") {
      return a + b;
    }
    if (Array.isArray(a) && Array.isArray(b)) {
      return [...a, ...b];
    }
  }
}

const calc = new Calculator();
console.log(calc.add(1, 2));           // 3
console.log(calc.add("Hello, ", "World!")); // "Hello, World!"
console.log(calc.add([1, 2], [3, 4])); // [1, 2, 3, 4]

Practical Examples#

Singleton Pattern#

class Database {
  private static instance: Database;
  private connected: boolean = false;

  private constructor() {
    // Private constructor prevents direct instantiation
  }

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

  connect(): void {
    if (!this.connected) {
      console.log("Connecting to database...");
      this.connected = true;
    }
  }

  disconnect(): void {
    if (this.connected) {
      console.log("Disconnecting from database...");
      this.connected = false;
    }
  }
}

// const db = new Database();  // ❌ Error: Constructor is private
const db1 = Database.getInstance();
const db2 = Database.getInstance();
console.log(db1 === db2);  // true - same instance

Repository Pattern#

abstract class BaseRepository<T> {
  protected items: T[] = [];

  add(item: T): void {
    this.items.push(item);
  }

  getAll(): T[] {
    return [...this.items];
  }

  abstract findById(id: string | number): T | undefined;
  abstract delete(id: string | number): boolean;
}

interface User {
  id: number;
  name: string;
  email: string;
}

class UserRepository extends BaseRepository<User> {
  findById(id: number): User | undefined {
    return this.items.find(user => user.id === id);
  }

  delete(id: number): boolean {
    const index = this.items.findIndex(user => user.id === id);
    if (index !== -1) {
      this.items.splice(index, 1);
      return true;
    }
    return false;
  }

  findByEmail(email: string): User | undefined {
    return this.items.find(user => user.email === email);
  }
}

const userRepo = new UserRepository();
userRepo.add({ id: 1, name: "Alice", email: "alice@example.com" });
userRepo.add({ id: 2, name: "Bob", email: "bob@example.com" });

Event Emitter#

type EventHandler<T = any> = (data: T) => void;

class EventEmitter {
  private events: Map<string, EventHandler[]> = new Map();

  on(event: string, handler: EventHandler): () => void {
    if (!this.events.has(event)) {
      this.events.set(event, []);
    }
    this.events.get(event)!.push(handler);

    // Return unsubscribe function
    return () => this.off(event, handler);
  }

  off(event: string, handler: EventHandler): void {
    const handlers = this.events.get(event);
    if (handlers) {
      const index = handlers.indexOf(handler);
      if (index !== -1) {
        handlers.splice(index, 1);
      }
    }
  }

  emit(event: string, data?: any): void {
    const handlers = this.events.get(event);
    if (handlers) {
      handlers.forEach(handler => handler(data));
    }
  }

  once(event: string, handler: EventHandler): void {
    const onceHandler: EventHandler = (data) => {
      handler(data);
      this.off(event, onceHandler);
    };
    this.on(event, onceHandler);
  }
}

// Usage
const emitter = new EventEmitter();

const unsubscribe = emitter.on("userLogin", (data) => {
  console.log(`User logged in:`, data);
});

emitter.emit("userLogin", { userId: "123", timestamp: Date.now() });

unsubscribe(); // Stop listening

Builder Pattern#

class HttpRequest {
  private url: string = "";
  private method: string = "GET";
  private headers: Record<string, string> = {};
  private body: any = null;

  setUrl(url: string): this {
    this.url = url;
    return this;
  }

  setMethod(method: string): this {
    this.method = method;
    return this;
  }

  addHeader(key: string, value: string): this {
    this.headers[key] = value;
    return this;
  }

  setBody(body: any): this {
    this.body = body;
    return this;
  }

  async send(): Promise<Response> {
    return fetch(this.url, {
      method: this.method,
      headers: this.headers,
      body: this.body ? JSON.stringify(this.body) : undefined
    });
  }
}

// Usage
const response = await new HttpRequest()
  .setUrl("https://api.example.com/users")
  .setMethod("POST")
  .addHeader("Content-Type", "application/json")
  .addHeader("Authorization", "Bearer token")
  .setBody({ name: "Alice", email: "alice@example.com" })
  .send();

Best Practices#

  1. Use parameter properties for concise code

    constructor(public name: string, private age: number) {}
    

  2. Prefer composition over inheritance

    class Car {
      constructor(private engine: Engine) {}
    }
    

  3. Use access modifiers appropriately

    class User {
      private password: string;  // Hide sensitive data
      public name: string;       // Public API
    }
    

  4. Make classes immutable with readonly

    class Config {
      constructor(readonly apiKey: string) {}
    }
    

  5. Use abstract classes for shared behavior

    abstract class BaseService {
      abstract getData(): Promise<any>;
    }
    

  6. Avoid deep inheritance hierarchies

    // ❌ Too deep
    class A {}
    class B extends A {}
    class C extends B {}
    class D extends C {}
    

Next Steps#