DEV Community

Cover image for Abstract Classes in TypeScript: Enforcing an Architectural Imperative
Duško Perić
Duško Perić

Posted on

Abstract Classes in TypeScript: Enforcing an Architectural Imperative

When developing robust and scalable software applications, introducing abstract classes in TypeScript is often a key architectural decision. An abstract class is the right solution when we want to define a shared foundation for a group of related objects, providing common, already implemented state and behavior (the DRY principle). In other words, an abstract class defines what its subclasses share, and what they are required to implement in order to be concrete and functional.

Design Balance: Implemented Methods and Abstract Requirements

The strength of an abstract class lies in its ability to balance shared implementation with required customization.
This division is expressed through two components that coexist within an abstract structure:

  1. Shared Behavior (Concrete Methods and Properties)
    This part of the abstract class includes all methods and properties that are fully functional and implemented. Subclasses automatically inherit this implementation without needing to rewrite it.

  2. Abstract Imperative (Required Methods)
    These are methods that define a signature but lack an implementation. By declaring a method as abstract, we are not offering an option, we are enforcing that every subclass must provide its own implementation.

abstract class ApiClient<T> {
  // Shared behavior
  async getAll(): Promise<T[]> {
    const res = await fetch(this.getEndpoint());
    return res.json();
  }

  // Required behavior: each subclass MUST return its own endpoint
  protected abstract getEndpoint(): string;
}

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

// Concrete class for users
class UserClient extends ApiClient<User> {
  protected getEndpoint(): string {
    return "/api/users";
  }
}

// Concrete class for products
class ProductClient extends ApiClient<Product> {
  protected getEndpoint(): string {
    return "/api/products";
  }
}

// Usage
const users = new UserClient();
users.getAll()

const products = new ProductClient();
products.getAll()

Enter fullscreen mode Exit fullscreen mode

Design Control and Discipline

The greatest contribution of an abstract class is its ability to enforce structural discipline and protect the codebase from inconsistencies. Abstract classes ensure that:

  • Key functionality is never forgotten: Declaring an essential method as abstract guarantees that no subclass can be created without providing a concrete implementation of that method.

  • Consistent Behavior (Polymorphism): Different subclasses can provide different implementations, but from the outside they can all be treated uniformly through the shared abstract type.

Abstraction serves as the first line of defense in complex systems, ensuring that the core structure and identity of an object aren’t bypassed or ignored.

Using abstract classes is a way to ensure a clear architectural structure: shared logic is centralized, and essential behavior cannot be omitted. The result is more organized, readable, and stable code.

Top comments (0)