TypeScript Abstract Classes

Creating templates for other classes

🎨 What are Abstract Classes?

Abstract classes are templates that cannot be instantiated directly. They define common structure and behavior that child classes must implement, ensuring consistency while allowing customization. They serve as blueprints for related classes.


// Abstract class
abstract class Animal {
    constructor(public name: string) {}

    // Abstract method (must be implemented)
    abstract makeSound(): string;

    // Concrete method (inherited as-is)
    move() {
        return `${this.name} is moving`;
    }
}

class Dog extends Animal {
    makeSound() {
        return "Woof!";
    }
}

const dog = new Dog("Buddy");
console.log(dog.makeSound());
console.log(dog.move());
                                    

Output:

Woof!

Buddy is moving

Abstract Class Features

🚫

No Instantiation

Cannot create objects directly

abstract class Base {}
// new Base(); // Error!
📋

Abstract Methods

Methods without implementation

abstract class Shape {
    abstract getArea(): number;
}
✅

Concrete Methods

Fully implemented methods

abstract class Animal {
    move() {
        return "Moving...";
    }
}
🔗

Inheritance

Child classes must implement abstracts

class Cat extends Animal {
    makeSound() {
        return "Meow";
    }
}

🔹 Basic Abstract Class

Define an abstract class with abstract and concrete methods:

abstract class Vehicle {
    constructor(
        public brand: string,
        public model: string
    ) {}

    // Abstract method - must be implemented by child classes
    abstract startEngine(): string;

    // Concrete method - inherited as-is
    displayInfo() {
        return `${this.brand} ${this.model}`;
    }
}

class Car extends Vehicle {
    startEngine() {
        return `${this.brand} car engine started with key`;
    }
}

class Motorcycle extends Vehicle {
    startEngine() {
        return `${this.brand} motorcycle engine started with button`;
    }
}

const car = new Car("Toyota", "Camry");
const bike = new Motorcycle("Honda", "CBR");

console.log(car.displayInfo());
console.log(car.startEngine());
console.log(bike.startEngine());

Output:

Toyota Camry

Toyota car engine started with key

Honda motorcycle engine started with button

🔹 Abstract Class with Multiple Abstract Methods

Child classes must implement all abstract methods:

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

    abstract getArea(): number;
    abstract getPerimeter(): number;

    describe() {
        return `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 * this.radius;
    }

    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 circle = new Circle("red", 5);
const rect = new Rectangle("blue", 4, 6);

console.log(circle.describe());
console.log(`Circle perimeter: ${circle.getPerimeter().toFixed(2)}`);
console.log(`Rectangle area: ${rect.getArea()}`);

Output:

A red shape with area 78.53981633974483

Circle perimeter: 31.42

Rectangle area: 24

🔹 Abstract Class for Database Operations

Use abstract classes to define common patterns:

abstract class Database {
    constructor(public connectionString: string) {}

    // Abstract methods
    abstract connect(): string;
    abstract disconnect(): string;
    abstract query(sql: string): string;

    // Concrete method
    logOperation(operation: string) {
        return `[LOG] ${operation} at ${new Date().toISOString()}`;
    }
}

class MySQLDatabase extends Database {
    connect() {
        return `Connected to MySQL: ${this.connectionString}`;
    }

    disconnect() {
        return "MySQL connection closed";
    }

    query(sql: string) {
        return `Executing MySQL query: ${sql}`;
    }
}

class MongoDatabase extends Database {
    connect() {
        return `Connected to MongoDB: ${this.connectionString}`;
    }

    disconnect() {
        return "MongoDB connection closed";
    }

    query(sql: string) {
        return `Executing MongoDB query: ${sql}`;
    }
}

const mysql = new MySQLDatabase("localhost:3306");
console.log(mysql.connect());
console.log(mysql.query("SELECT * FROM users"));
console.log(mysql.logOperation("Query executed"));

Output:

Connected to MySQL: localhost:3306

Executing MySQL query: SELECT * FROM users

[LOG] Query executed at 2024-01-15T10:30:00.000Z

🔹 Abstract Properties

Abstract classes can have abstract properties using getters/setters:

abstract class Employee {
    constructor(public name: string) {}

    // Abstract getter
    abstract get salary(): number;
    
    // Abstract method
    abstract calculateBonus(): number;

    // Concrete method
    getTotalCompensation() {
        return this.salary + this.calculateBonus();
    }
}

class FullTimeEmployee extends Employee {
    constructor(
        name: string,
        private baseSalary: number
    ) {
        super(name);
    }

    get salary(): number {
        return this.baseSalary;
    }

    calculateBonus(): number {
        return this.baseSalary * 0.1;
    }
}

class Contractor extends Employee {
    constructor(
        name: string,
        private hourlyRate: number,
        private hoursWorked: number
    ) {
        super(name);
    }

    get salary(): number {
        return this.hourlyRate * this.hoursWorked;
    }

    calculateBonus(): number {
        return 0; // Contractors don't get bonuses
    }
}

const fullTime = new FullTimeEmployee("Alice", 80000);
const contractor = new Contractor("Bob", 50, 160);

console.log(`${fullTime.name} total: $${fullTime.getTotalCompensation()}`);
console.log(`${contractor.name} total: $${contractor.getTotalCompensation()}`);

Output:

Alice total: $88000

Bob total: $8000

🔹 When to Use Abstract Classes

Use Abstract Classes When:

  • You want to share code among related classes
  • You need to define common behavior with some implementation
  • Child classes should follow a specific structure
  • You want to provide default implementations for some methods

Don't Use Abstract Classes When:

  • You only need to define a contract (use interfaces instead)
  • You need multiple inheritance (TypeScript doesn't support it)
  • Classes are unrelated and don't share behavior

🧠 Test Your Knowledge

Can you create an instance of an abstract class directly?