TypeScript Abstract Classes

An abstract class is a blueprint that cannot be used directly. It defines a structure — what methods every child class must have — without providing all the details of those methods. Think of it as a half-finished template. The child class fills in the missing parts and becomes the usable, complete version.

The Blueprint Analogy

ARCHITECT'S BLUEPRINT (Abstract Class)
  ├── Room dimensions → defined (shared rules)
  ├── Electrical wiring → defined (shared rules)
  ├── Interior decoration → NOT defined (each owner decides)
  └── Furniture layout → NOT defined (each owner decides)

ACTUAL HOUSE (Child Class)
  ├── Room dimensions → inherits from blueprint
  ├── Wiring → inherits from blueprint
  ├── Interior decoration → owner fills in
  └── Furniture layout → owner fills in

The blueprint (abstract class) cannot become a house by itself — it needs a builder (child class) to complete it.

Declaring an Abstract Class

The abstract keyword marks a class as abstract. Methods inside it marked with abstract have no body — child classes must provide the body.

abstract class Shape {
    // Regular method — shared by all shapes
    public describe(): string {
        return `I am a ${this.constructor.name}`;
    }

    // Abstract method — each shape must define its own
    abstract getArea(): number;
    abstract getPerimeter(): number;
}

// Cannot instantiate directly
// let s = new Shape(); // Error: Cannot create an instance of an abstract class

Implementing Abstract Classes

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

    public 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 * this.radius;
    }

    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);
    }
}

let circle = new Circle(7);
let rect   = new Rectangle(10, 5);

console.log(circle.describe());
// Area: 153.94, Perimeter: 43.98

console.log(rect.describe());
// Area: 50, Perimeter: 30

How It Works Step by Step

abstract Shape
  ├── abstract getArea()       ← no body, child must add one
  ├── abstract getPerimeter()  ← no body, child must add one
  └── describe()               ← has body, shared as-is
          │
          ├── Circle (child)
          │     ├── getArea()      ← radius * radius * π
          │     └── getPerimeter() ← 2 * π * radius
          │
          └── Rectangle (child)
                ├── getArea()      ← width * height
                └── getPerimeter() ← 2 * (width + height)

Abstract vs Regular vs Interface

FeatureAbstract ClassRegular ClassInterface
Can be instantiatedNoYesNo
Can have method bodiesYes (some)Yes (all)No
Can have constructorsYesYesNo
Can have access modifiersYesYesNo
Forces child to implement methodsYesNoYes
Supports multiple inheritanceNo (one parent)No (one parent)Yes (many interfaces)

Real-World Example: Payment System

abstract class PaymentGateway {
    protected transactionId: string;

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

    // Shared method — same for every gateway
    public logTransaction(): void {
        console.log(`Transaction ID: ${this.transactionId} processed.`);
    }

    // Each gateway handles payment its own way
    abstract processPayment(amount: number): string;
    abstract refund(amount: number): string;
}

class UPIGateway extends PaymentGateway {
    constructor(txId: string, private upiId: string) {
        super(txId);
    }

    processPayment(amount: number): string {
        return `UPI ${this.upiId}: ₹${amount} paid. TxID: ${this.transactionId}`;
    }

    refund(amount: number): string {
        return `UPI refund of ₹${amount} to ${this.upiId}`;
    }
}

class CardGateway extends PaymentGateway {
    constructor(txId: string, private last4Digits: string) {
        super(txId);
    }

    processPayment(amount: number): string {
        return `Card ****${this.last4Digits}: ₹${amount} charged.`;
    }

    refund(amount: number): string {
        return `Card refund of ₹${amount} to ****${this.last4Digits}`;
    }
}

let upi  = new UPIGateway("TX001", "rahul@upi");
let card = new CardGateway("TX002", "4242");

console.log(upi.processPayment(1500));
// UPI rahul@upi: ₹1500 paid. TxID: TX001

console.log(card.processPayment(3200));
// Card ****4242: ₹3200 charged.

upi.logTransaction();
// Transaction ID: TX001 processed.

Abstract Class with Constructor Logic

abstract class Vehicle {
    public readonly make: string;
    public readonly model: string;
    protected fuelLevel: number = 100;

    constructor(make: string, model: string) {
        this.make = make;
        this.model = model;
    }

    public refuel(): void {
        this.fuelLevel = 100;
        console.log(`${this.make} ${this.model} refueled.`);
    }

    abstract startEngine(): string;
    abstract getFuelEfficiency(): string;
}

class ElectricCar extends Vehicle {
    startEngine(): string {
        return `${this.make} ${this.model} starts silently (electric).`;
    }

    getFuelEfficiency(): string {
        return "250 km per charge";
    }
}

class PetrolCar extends Vehicle {
    startEngine(): string {
        return `${this.make} ${this.model} engine roars!`;
    }

    getFuelEfficiency(): string {
        return "15 km per litre";
    }
}

let tesla = new ElectricCar("Tesla", "Model 3");
let swift = new PetrolCar("Maruti", "Swift");

console.log(tesla.startEngine());
// Tesla Model 3 starts silently (electric).

console.log(swift.getFuelEfficiency());
// 15 km per litre

swift.refuel();
// Maruti Swift refueled.

Using Abstract Class as a Type

Even though an abstract class cannot be instantiated, it works as a type. A variable typed as the abstract class can hold any of its child class instances.

abstract class Animal {
    abstract makeSound(): string;

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

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

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

// Animal as a type — holds any subclass
let animals: Animal[] = [new Dog(), new Cat(), new Dog()];

for (let animal of animals) {
    console.log(animal.makeSound());
}
// Woof!
// Meow!
// Woof!

Key Rules

RuleDetail
Use abstract keywordBefore the class keyword and before method names
No instantiationnew AbstractClass() gives a compile error
Child class must implement all abstract methodsMissing any one gives a compile error
Non-abstract methods are inherited normallyChild classes get them without re-defining
Can be used as a typeVariables and arrays can have the abstract class as their type

Leave a Comment

Your email address will not be published. Required fields are marked *