Skip to content

TypeScript Abstract Classes — Complete Guide

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about TypeScript Abstract Classes. We cover key concepts, practical examples, and best practices to help you master this topic.

TypeScript abstract classes define base behavior that subclasses must implement — they are blueprints that enforce a contract while providing shared functionality, combining the best of interfaces and concrete classes.

What You'll Learn

  • Abstract class syntax and purpose
  • Abstract methods and properties
  • Constructors in abstract classes
  • Static members in abstract classes
  • The Template Method design pattern

Why It Matters

Abstract classes solve the problem of "shared logic with required customization." Unlike interfaces (which only describe shape) or concrete classes (which provide full implementation), abstract classes let you define what's common while forcing subclasses to implement what's specific.

Real-World Use

Durga Antivirus Pro's plugin system uses an abstract ScannerPlugin class — every scanner (FileScanner, NetworkScanner, EmailScanner) must implement scan() and getName(), but they all share the logging and reporting logic defined in the base class.

Learning Path

flowchart LR
  A[Classes] --> B[Abstract Classes]
  B --> C[Inheritance]
  B --> D[You Are Here]
  C --> E[Decorators]
  E --> F[This Typing]

Abstract Class Basics

An abstract class cannot be instantiated directly — it must be subclassed:

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

  describe(): string {
    return `Area: ${this.getArea().toFixed(2)}, Perimeter: ${this.getPerimeter().toFixed(2)}`;
  }
}

// const shape = new Shape(); // Error: Cannot create an instance of an abstract class

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

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

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

const circle = new Circle(5);
console.log(circle.describe());
// Area: 78.54, Perimeter: 31.42

Think of an abstract class like a course syllabus — it lists what you'll learn (abstract methods) and provides the reading materials (concrete methods), but each student implements their learning differently.

Abstract Methods

Abstract methods have no body — subclasses must implement them:

abstract class DatabaseAdapter {
  abstract connect(): Promise<void>;
  abstract disconnect(): Promise<void>;
  abstract query<T>(sql: string): Promise<T[]>;

  async executeTransaction<T>(queries: (() => Promise<T>)[]): Promise<T[]> {
    await this.connect();
    const results: T[] = [];
    try {
      for (const query of queries) {
        results.push(await query());
      }
    } finally {
      await this.disconnect();
    }
    return results;
  }
}

class PostgresAdapter extends DatabaseAdapter {
  async connect(): Promise<void> {
    console.log("Connecting to PostgreSQL...");
  }

  async disconnect(): Promise<void> {
    console.log("Disconnecting from PostgreSQL...");
  }

  async query<T>(sql: string): Promise<T[]> {
    console.log(`Executing: ${sql}`);
    return [];
  }
}

const db = new PostgresAdapter();
db.executeTransaction([
  () => db.query("SELECT * FROM users"),
]);

Abstract Properties

Abstract classes can declare abstract properties that subclasses must provide:

abstract class Report {
  abstract readonly title: string;
  abstract readonly maxRows: number;

  abstract generate(): string[];

  print(): void {
    const lines = this.generate().slice(0, this.maxRows);
    console.log(`=== ${this.title} ===`);
    for (const line of lines) {
      console.log(line);
    }
  }
}

class ScanReport extends Report {
  readonly title = "Scan Results";
  readonly maxRows = 10;

  generate(): string[] {
    return [
      "File: document.pdf — Clean",
      "File: setup.exe — Trojan.Generic detected",
      "File: photo.jpg — Clean",
    ];
  }
}

const report = new ScanReport();
report.print();
// === Scan Results ===
// File: document.pdf — Clean
// File: setup.exe — Trojan.Generic detected
// File: photo.jpg — Clean

Constructors in Abstract Classes

Abstract classes can have constructors that subclasses must call via super():

abstract class ApiClient {
  constructor(
    protected baseUrl: string,
    protected timeout: number = 5000
  ) {
    this.validateUrl();
  }

  private validateUrl(): void {
    if (!this.baseUrl.startsWith("http")) {
      throw new Error("Invalid URL");
    }
  }

  abstract get<T>(path: string): Promise<T>;
  abstract post<T>(path: string, body: unknown): Promise<T>;
}

class ScanApiClient extends ApiClient {
  constructor(baseUrl: string) {
    super(baseUrl, 10000); // 10 second timeout for scans
  }

  async get<T>(path: string): Promise<T> {
    const response = await fetch(`${this.baseUrl}${path}`, {
      signal: AbortSignal.timeout(this.timeout),
    });
    return response.json();
  }

  async post<T>(path: string, body: unknown): Promise<T> {
    const response = await fetch(`${this.baseUrl}${path}`, {
      method: "POST",
      body: JSON.stringify(body),
      headers: { "Content-Type": "application/json" },
      signal: AbortSignal.timeout(this.timeout),
    });
    return response.json();
  }
}

const client = new ScanApiClient("https://api.dodatech.com");
// client.get("/scans"); // Fully typed

Static Members

Static members belong to the class itself, not instances:

abstract class ConfigManager {
  private static configs: Map<string, unknown> = new Map();

  static set<T>(key: string, value: T): void {
    this.configs.set(key, value);
  }

  static get<T>(key: string): T | undefined {
    return this.configs.get(key) as T;
  }

  static has(key: string): boolean {
    return this.configs.has(key);
  }

  abstract validate(): boolean;
}

ConfigManager.set("apiUrl", "https://api.example.com");
ConfigManager.set("timeout", 5000);

const url = ConfigManager.get<string>("apiUrl");
const timeout = ConfigManager.get<number>("timeout");

console.log(url, timeout); // https://api.example.com 5000

Template Method Pattern

Abstract classes naturally implement the Template Method pattern — define the skeleton of an algorithm, letting subclasses fill in the details:

abstract class DataImporter {
  // Template method — defines the algorithm skeleton
  async import(source: string): Promise<{ imported: number; errors: string[] }> {
    const errors: string[] = [];
    let imported = 0;

    try {
      const raw = await this.fetchData(source);
      const valid = this.validate(raw);
      const transformed = this.transform(valid);
      imported = await this.save(transformed);
    } catch (error) {
      errors.push(`Import failed: ${error}`);
    }

    await this.cleanup();
    return { imported, errors };
  }

  // Steps that subclasses must implement
  protected abstract fetchData(source: string): Promise<unknown[]>;
  protected abstract validate(data: unknown[]): unknown[];
  protected abstract transform(data: unknown[]): unknown[];
  protected abstract save(data: unknown[]): Promise<number>;

  // Optional hook
  protected cleanup(): Promise<void> {
    return Promise.resolve();
  }
}

class CSVSecurityScanner extends DataImporter {
  protected async fetchData(source: string): Promise<unknown[]> {
    console.log(`Reading CSV: ${source}`);
    return [{ name: "malware.exe", hash: "abc123" }];
  }

  protected validate(data: unknown[]): unknown[] {
    return data.filter(entry => entry !== null);
  }

  protected transform(data: unknown[]): unknown[] {
    return data.map((entry: any) => ({
      ...entry,
      scanned: true,
      scannedAt: new Date().toISOString(),
    }));
  }

  protected async save(data: unknown[]): Promise<number> {
    console.log(`Saved ${data.length} records`);
    return data.length;
  }
}

Abstract Class vs Interface

Feature Abstract Class Interface
Can have implementation Yes No
Constructor Yes No
Access modifiers Yes (private, protected) No
Multiple inheritance No (single extends) Yes (multiple implements)
Static members Yes No
Runtime presence Yes (compiles to class) No (erased)

Rule of thumb: Use interfaces for contracts (what). Use abstract classes for shared behavior (how + what).

Common Mistakes

1. Trying to Instantiate an Abstract Class

abstract class Animal {}
// const animal = new Animal(); // Error: Cannot create an instance of an abstract class

2. Forgetting to Call super() in Subclass Constructor

abstract class Base {
  constructor(protected name: string) {}
}

class Derived extends Base {
  constructor(name: string) {
    super(name); // Must call super first!
  }
}

3. Not Implementing All Abstract Methods

abstract class Logger {
  abstract log(message: string): void;
  abstract error(message: string): void;
}

// class ConsoleLogger extends Logger { } // Error: missing abstract methods
class ConsoleLogger extends Logger {
  log(message: string): void { console.log(message); }
  error(message: string): void { console.error(message); }
}

4. Using Abstract When an Interface Would Suffice

If you only need to describe a contract without any shared implementation, use an interface.

5. Making Methods That Should Be Concrete, Abstract

abstract class Database {
  // This shouldn't be abstract — it's the same for all databases
  abstract formatConnectionString(host: string, port: number): string {
    return `${host}:${port}`;
  }
  // Error: Abstract methods cannot have implementation
}

Practice Questions

  1. Can you create an instance of an abstract class? No. Abstract classes must be subclassed. Only concrete subclasses can be instantiated.

  2. What must a subclass do with abstract methods? Implement all of them (unless the subclass is also abstract).

  3. Can abstract classes have constructors? Yes, and subclasses must call super() with the required arguments.

  4. What is the Template Method pattern? An abstract class defines the skeleton of an algorithm as a concrete method, with abstract methods for the steps that subclasses customize.

Challenge: Create an abstract PaymentProcessor class with abstract authorize(amount: number): boolean and capture(authorizationId: string): boolean methods, plus a concrete processPayment(amount: number) template method. Implement two concrete classes: CreditCardProcessor and PayPalProcessor.

FAQ

Can an abstract class implement an interface?

Yes: abstract class MyClass implements MyInterface { ... }

Can I have both abstract and concrete methods in the same class?

Yes, that's the whole point of abstract classes — some methods are implemented, others are abstract.

Can abstract classes have private methods?

Yes. Private concrete methods are common for internal helper logic.

What happens if I extend an abstract class but don't implement all abstract methods?

The compiler will error. Either implement all abstract methods or mark the subclass as abstract too.

Are abstract methods faster than regular methods?

No. Abstract methods add compile-time constraints but don't affect runtime performance.

Mini Project: Plugin System

// src/plugin-system.ts

abstract class ScannerPlugin {
  abstract readonly name: string;
  abstract readonly version: string;

  abstract scan(target: string): Promise<ScanResult>;
  abstract getDescription(): string;

  protected log(message: string): void {
    console.log(`[${this.name} v${this.version}] ${message}`);
  }

  async run(target: string): Promise<ScanResult> {
    this.log(`Starting scan of ${target}`);
    const startTime = Date.now();
    try {
      const result = await this.scan(target);
      result.duration = Date.now() - startTime;
      this.log(`Scan complete: ${result.threats} threats found in ${result.duration}ms`);
      return result;
    } catch (error) {
      this.log(`Scan failed: ${error}`);
      throw error;
    }
  }
}

interface ScanResult {
  target: string;
  threats: number;
  duration: number;
  details: string[];
}

class FileScanner extends ScannerPlugin {
  readonly name = "File Scanner";
  readonly version = "1.0.0";

  getDescription(): string {
    return "Scans files for malware signatures";
  }

  async scan(target: string): Promise<ScanResult> {
    return {
      target,
      threats: 2,
      duration: 0,
      details: ["Trojan.Generic detected in setup.exe", "Adware.Bundle detected in installer.msi"],
    };
  }
}

class NetworkScanner extends ScannerPlugin {
  readonly name = "Network Scanner";
  readonly version = "2.3.1";

  getDescription(): string {
    return "Monitors network traffic for suspicious patterns";
  }

  async scan(target: string): Promise<ScanResult> {
    return {
      target,
      threats: 0,
      duration: 0,
      details: ["No threats detected"],
    };
  }
}

async function runAllScanners(targets: string[], scanners: ScannerPlugin[]) {
  for (const scanner of scanners) {
    for (const target of targets) {
      const result = await scanner.run(target);
      console.log(`${scanner.name}: ${result.target}${result.threats} threats`);
    }
  }
}

const scanners: ScannerPlugin[] = [new FileScanner(), new NetworkScanner()];
runAllScanners(["/home/user/docs", "/tmp/downloads"], scanners);

What's Next

Now explore inheritance and mixins:

Lesson Description
{{< ref "/programming-languages/typescript/19-classes" >}} Review class basics
{{< ref "/programming-languages/typescript/21-inheritance" >}} extends, super, method overriding, mixins
{{< ref "/programming-languages/typescript/22-decorators" >}} Class and method decorators

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro