Abstract Classes

Difficulty: Intermediate

Abstract classes in TypeScript serve as base classes that cannot be instantiated directly. They provide a way to define a common structure and shared behavior that subclasses must follow. You declare an abstract class using the `abstract` keyword before the class definition, and any class that extends it must implement all of its abstract methods.

Abstract methods are method signatures declared with the `abstract` keyword but without an implementation body. They define a contract: every concrete (non-abstract) subclass must provide its own implementation. This is powerful for defining families of related classes that share common behavior but differ in specific details.

Unlike interfaces, abstract classes can include fully implemented methods alongside abstract ones. This means you can put shared logic in the base class and only require subclasses to fill in the parts that vary. This pattern is known as the Template Method pattern in design patterns terminology and is extremely common in real-world applications.

Abstract classes also support constructors, properties with access modifiers, and everything else a regular class supports. The key difference is simply that you cannot do `new AbstractClass()` directly - you must create a concrete subclass. This makes abstract classes ideal for defining frameworks or base layers where the specific behavior is determined by subclasses.

Code examples

Basic Abstract Class

abstract class Shape {
  abstract area(): number;
  abstract perimeter(): number;

  describe(): string {
    return `Area: ${this.area()}, Perimeter: ${this.perimeter()}`;
  }
}

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

  area(): number {
    return Math.round(Math.PI * this.radius  2 * 100) / 100;
  }

  perimeter(): number {
    return Math.round(2 * Math.PI * this.radius * 100) / 100;
  }
}

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

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

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

const shapes: Shape[] = [new Circle(5), new Rectangle(4, 6)];
shapes.forEach(s => console.log(s.describe()));

Shape defines two abstract methods (area and perimeter) and one concrete method (describe) that uses them. Circle and Rectangle each provide their own implementations.

Abstract Class with Constructor and Properties

abstract class Vehicle {
  constructor(protected brand: string, protected year: number) {}

  abstract fuelType(): string;

  info(): string {
    return `${this.year} ${this.brand} (${this.fuelType()})`;
  }
}

class ElectricCar extends Vehicle {
  fuelType(): string {
    return "Electric";
  }
}

class GasCar extends Vehicle {
  fuelType(): string {
    return "Gasoline";
  }
}

const cars: Vehicle[] = [
  new ElectricCar("Tesla", 2024),
  new GasCar("Toyota", 2023)
];
cars.forEach(c => console.log(c.info()));

The Vehicle abstract class has a constructor with protected properties and a concrete info() method. Each subclass only needs to implement the abstract fuelType() method.

Key points

Concepts covered

abstract class, abstract method, inheritance, polymorphism, template method pattern