TypeScript Abstract Classes — Complete Guide
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
Can you create an instance of an abstract class? No. Abstract classes must be subclassed. Only concrete subclasses can be instantiated.
What must a subclass do with abstract methods? Implement all of them (unless the subclass is also abstract).
Can abstract classes have constructors? Yes, and subclasses must call
super()with the required arguments.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
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