Abstract classes in TypeScript let you define a shared structure for a group of related classes without committing to a full implementation upfront. They're one of those features that feels academic until you're maintaining a codebase with six slightly-different-but-should-be-consistent classes, and you realize you needed them three months ago.

Understanding Abstract Classes

An abstract class cannot be instantiated directly. It is a blueprint that other classes inherit from. It can contain fully implemented methods alongside abstract methods, which are declarations with no body that subclasses must implement.

TypeScript uses the abstract keyword to mark both the class and any methods that require implementation in subclasses.

Abstract Classes in TypeScript

Here's a simple example:

abstract class Animal {
  abstract makeSound(): void; // Abstract method

  move(): void {
    console.log("Moving along!");
  }
}

Animal is abstract: it declares makeSound() without implementing it, and provides a concrete move() method. Any class extending Animal must supply its own makeSound().

Here's how you might extend the Animal class:

class Dog extends Animal {
  makeSound(): void {
    console.log("Bark!");
  }
}

const myDog = new Dog();
myDog.makeSound(); // Outputs: Bark!
myDog.move(); // Outputs: Moving along!

Dog gets move() for free and satisfies the makeSound() contract. Try to instantiate Animal directly and the TypeScript compiler rejects it at build time, before a bug ever reaches production.

When to Use Abstract Classes

Abstract classes work best when a group of classes shares real implementation, not just a contract. A common example is vehicles with different startup sequences:

abstract class Vehicle {
  abstract startEngine(): void;

  stopEngine(): void {
    console.log("Engine stopped.");
  }
}

Car and Motorcycle would each implement startEngine() differently, but they both inherit the same stopEngine() without duplication.

If the classes don't share any implementation, reach for an interface instead.

Advantages of Using Abstract Classes

  • Shared logic lives in one place. Change stopEngine() once and every subclass gets the update.
  • Polymorphism: code that references the abstract type works with any concrete subclass.
  • The compiler enforces the contract. Missing implementations are compile errors, not runtime surprises.

Abstract Classes vs. Interfaces

Both define contracts, but the distinction matters in practice.

Abstract classes can hold implemented methods and instance state. Interfaces are purely structural: they declare what a type must look like, with no implementation at all.

Pick an abstract class when you have shared code to distribute. Pick an interface when you're describing a capability that unrelated classes might adopt independently.

Real-World Example: Implementing a Shape Hierarchy

Consider an application that deals with geometric shapes. You can define an abstract class Shape:

abstract class Shape {
  abstract area(): number;

  toString(): string {
    return `Shape with area ${this.area()}`;
  }
}

Subclasses like Circle and Rectangle would implement the area() method:

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

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

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

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

const circle = new Circle(5);
console.log(circle.toString()); // Outputs: Shape with area 78.53981633974483

const rectangle = new Rectangle(10, 5);
console.log(rectangle.toString()); // Outputs: Shape with area 50

toString() is written once in Shape and calls whichever area() the concrete class provides. Adding a Triangle later means implementing one method, not copying boilerplate.

Conclusion

Abstract classes are the right tool when you have shared implementation to distribute and a contract to enforce. They catch missing method implementations at compile time and keep common logic in a single location. If you only need to describe a shape, use an interface. If you need to share code and enforce a shape, use an abstract class.