Skip to main content

TS Ep 22: Abstract Classes & Template Method Pattern

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
typescript - This article is part of a series.
Part 22: This Article
An Abstract Class is a non-instantiable base class that serves as an architectural blueprint. It combines concrete shared logic (like a standard class) with required method implementation contracts (like an interface).

1. What is an Abstract Class?
#

You declare an abstract class by prefixing the class keyword with abstract.

Abstract classes cannot be directly instantiated using new. Their sole purpose is to be extended by concrete subclasses via extends.

abstract class BaseHttpClient {
  constructor(public readonly baseUrl: string) {}

  // Concrete method shared by all subclasses
  public buildFullUrl(endpoint: string): string {
    return `${this.baseUrl.replace(/\/$/, "")}/${endpoint.replace(/^\//, "")}`;
  }
}

// ❌ Compiler Error: Cannot create an instance of an abstract class.
// const client = new BaseHttpClient("https://api.acme.com");

// 🟢 WORKED: Derived concrete subclass can be instantiated!
class UserApiClient extends BaseHttpClient {
  public async fetchUser(id: string) {
    const url = this.buildFullUrl(`users/${id}`);
    console.log(`Fetching from: ${url}`);
  }
}

const api = new UserApiClient("https://api.acme.com");
api.fetchUser("usr_99"); // "Fetching from: https://api.acme.com/users/usr_99"

2. Abstract Methods and Properties
#

An abstract class can contain Abstract Members (marked with the abstract keyword).

Abstract methods and properties do not have an implementation body in the base class. They act as mandatory contracts that every derived subclass MUST implement.

abstract class DatabaseAdapter {
  // Abstract Property Contract
  public abstract readonly engineName: string;

  // Abstract Method Contract (No function body!)
  public abstract connect(): Promise<void>;
  public abstract executeQuery(query: string): Promise<unknown[]>;
}

// Subclass fulfilling the abstract contracts
class PostgresAdapter extends DatabaseAdapter {
  // Implementing abstract property
  public override readonly engineName = "PostgreSQL";

  // Implementing abstract methods
  public override async connect(): Promise<void> {
    console.log(`Connecting to ${this.engineName} engine...`);
  }

  public override async executeQuery(query: string): Promise<unknown[]> {
    console.log(`Executing SQL on ${this.engineName}: ${query}`);
    return [{ id: 101, result: "OK" }];
  }
}

If a subclass forgets to implement an abstract method or property, TypeScript throws a compile-time error:

// ❌ Compiler Error: Non-abstract class 'BadAdapter' does not implement 
// inherited abstract member 'executeQuery' from class 'DatabaseAdapter'.
class BadAdapter extends DatabaseAdapter {
  public override readonly engineName = "BadEngine";
  public override async connect() {}
  // Missing executeQuery()!
}

3. The Template Method Design Pattern
#

Abstract classes are the natural habitat for the classic Template Method Design Pattern.

In this pattern, the abstract base class defines the skeleton of an algorithm in a high-level method (the Template Method), while delegating specific sub-steps to abstract methods that subclasses must override.

abstract class DataPipeline {
  // The Template Method (Defines algorithm flow)
  public async runPipeline(): Promise<void> {
    console.log("[Pipeline]: Step 1 - Extracting raw data...");
    const rawData = await this.extractData();

    console.log("[Pipeline]: Step 2 - Transforming data payload...");
    const transformed = this.transformData(rawData);

    console.log("[Pipeline]: Step 3 - Loading into destination...");
    await this.loadData(transformed);

    console.log("[Pipeline]: Complete!");
  }

  // Abstract Sub-Steps (Subclasses MUST implement these specific details)
  protected abstract extractData(): Promise<string>;
  protected abstract transformData(raw: string): string[];
  protected abstract loadData(items: string[]): Promise<void>;
}

// Concrete CSV Pipeline implementation
class CsvDataPipeline extends DataPipeline {
  protected override async extractData(): Promise<string> {
    return "id,name\n1,Alice\n2,Bob";
  }

  protected override transformData(raw: string): string[] {
    return raw.split("\n").slice(1); // Exclude header line
  }

  protected override async loadData(items: string[]): Promise<void> {
    console.log(`Loaded ${items.length} records into Database.`);
  }
}

const pipeline = new CsvDataPipeline();
pipeline.runPipeline();

4. Abstract Classes vs Interfaces
#

Featureinterfaceabstract class
JS Compiled FootprintErased completely (0 bytes)Emits JS Class constructor function
Concrete Code ReuseNo (Cannot contain code implementation)Yes (Can contain concrete shared methods/fields)
InstantiabilityCannot be instantiatedCannot be instantiated
Class RelationshipImplemented via implements (Multiple)Extended via extends (Single parent only)
Access ModifiersOnly public declarationspublic, protected, private, abstract

Summary & Next Steps
#

In this episode:

  • We defined non-instantiable base blueprints using abstract class.
  • We enforced method implementation contracts using abstract methods and properties.
  • We built automated ETL pipelines using the Template Method Design Pattern.
  • We evaluated when to use Interfaces vs Abstract Classes.

In Episode 23: Static Members, we will explore class static fields, static methods, and static block initializers (static {})!

typescript - This article is part of a series.
Part 22: This Article