Abstract Classes
An abstract class defines a common blueprint for a family of related classes. It can contain both concrete (implemented) methods and abstract (unimplemented) methods that every subclass must implement.
You cannot instantiate an abstract class directly — it exists only to be extended. This is the key difference from a regular base class.
Think of an abstract class as a contract plus a partial implementation. Interfaces are pure contracts; regular classes are full implementations; abstract classes sit in between.
Declaring an abstract class
abstract class Shape {
// Abstract method — subclasses MUST implement this
abstract area(): number;
abstract perimeter(): number;
// Concrete method — shared by all subclasses, no override needed
describe(): string {
return `Area: ${this.area().toFixed(2)}, Perimeter: ${this.perimeter().toFixed(2)}`;
}
isLargerThan(other: Shape): boolean {
return this.area() > other.area();
}
}
// new Shape(); // Error: Cannot create an instance of an abstract class.
class Circle extends Shape {
constructor(private radius: number) { super(); }
area(): number { return Math.PI * this.radius ** 2; }
perimeter(): number { return 2 * Math.PI * this.radius; }
}
class Rectangle extends Shape {
constructor(private w: number, private h: number) { super(); }
area(): number { return this.w * this.h; }
perimeter(): number { return 2 * (this.w + this.h); }
}
const c = new Circle(5);
const r = new Rectangle(4, 6);
console.log(c.describe()); // Area: 78.54, Perimeter: 31.42
console.log(r.describe()); // Area: 24.00, Perimeter: 20.00
console.log(c.isLargerThan(r)); // trueabstract keyword on a method means subclasses must implement it — forgetting to do so is a compile-time error, not a runtime surprise.Abstract properties
You can also declare abstract properties — forcing each subclass to expose a particular field or getter.
abstract class Vehicle {
abstract readonly brand: string; // subclass must provide this
abstract readonly fuelType: string;
abstract startEngine(): void;
// Concrete shared behaviour
describe(): string {
return `${this.brand} runs on ${this.fuelType}`;
}
}
class ElectricCar extends Vehicle {
readonly brand = 'Tesla';
readonly fuelType = 'electricity';
startEngine(): void {
console.log('Silently started...');
}
}
class GasCar extends Vehicle {
constructor(
readonly brand: string,
readonly fuelType = 'gasoline',
) { super(); }
startEngine(): void {
console.log('Vroom!');
}
}
const cars: Vehicle[] = [new ElectricCar(), new GasCar('Toyota')];
cars.forEach(car => {
console.log(car.describe());
car.startEngine();
});Abstract classes vs interfaces
Both abstract classes and interfaces define contracts. The choice between them is a design decision:
Feature | Interface | Abstract class |
|---|---|---|
Can contain implementation | ❌ | ✅ |
Can have constructors | ❌ | ✅ |
Multiple inheritance | ✅ (a class can implement many) | ❌ (single extends) |
Can have private/protected members | ❌ | ✅ |
Runtime presence (emitted JS) | ❌ (erased) | ✅ (real class) |
Use when… | defining a pure shape/contract | sharing code + enforcing contract |
Constructor parameters in abstract classes
Abstract classes can have constructors with parameters. Subclasses must call super() to initialize the base.
abstract class Repository<T extends { id: number }> {
protected items: T[] = [];
constructor(protected readonly name: string) {
console.log(`Created repository: ${name}`);
}
findById(id: number): T | undefined {
return this.items.find(item => item.id === id);
}
findAll(): T[] {
return [...this.items]; // return a copy
}
abstract save(item: T): void;
abstract delete(id: number): boolean;
}
interface User { id: number; name: string; email: string; }
class UserRepository extends Repository<User> {
constructor() { super('users'); }
save(user: User): void {
const index = this.items.findIndex(u => u.id === user.id);
if (index >= 0) {
this.items[index] = user; // update
} else {
this.items.push(user); // insert
}
}
delete(id: number): boolean {
const before = this.items.length;
this.items = this.items.filter(u => u.id !== id);
return this.items.length < before;
}
findByEmail(email: string): User | undefined {
return this.items.find(u => u.email === email);
}
}
const repo = new UserRepository();
repo.save({ id: 1, name: 'Alice', email: 'alice@example.com' });
repo.save({ id: 2, name: 'Bob', email: 'bob@example.com' });
console.log(repo.findById(1)?.name); // Alice
console.log(repo.findAll().length); // 2
repo.delete(1);
console.log(repo.findAll().length); // 1Template Method pattern
The classic use of abstract classes is the Template Method design pattern: the abstract class defines the skeleton of an algorithm and lets subclasses fill in the steps.
abstract class DataProcessor {
// Template method — defines the algorithm skeleton
process(rawData: string): void {
const parsed = this.parse(rawData);
const cleaned = this.clean(parsed);
const result = this.transform(cleaned);
this.output(result);
}
// Steps that subclasses must implement
protected abstract parse(raw: string): unknown[];
protected abstract transform(data: unknown[]): unknown[];
// Steps with default implementations that subclasses can override
protected clean(data: unknown[]): unknown[] {
return data.filter(item => item !== null && item !== undefined);
}
protected output(result: unknown[]): void {
console.log(JSON.stringify(result, null, 2));
}
}
class CsvProcessor extends DataProcessor {
protected parse(raw: string): string[][] {
return raw.trim().split('\n').map(line => line.split(','));
}
protected transform(data: string[][]): Record<string, string>[] {
const [headers, ...rows] = data;
return rows.map(row =>
Object.fromEntries(headers.map((h, i) => [h.trim(), row[i]?.trim() ?? '']))
);
}
}
const csv = 'name,age\nAlice,30\nBob,25';
new CsvProcessor().process(csv);
// [{ "name": "Alice", "age": "30" }, { "name": "Bob", "age": "25" }]Protected abstract methods
Abstract methods can be protected, meaning they are implementation details of the class hierarchy — not part of the public API.
abstract class Logger {
log(message: string): void {
const formatted = this.format(message);
this.write(formatted);
}
protected abstract format(message: string): string;
protected abstract write(message: string): void;
}
class ConsoleLogger extends Logger {
protected format(msg: string): string {
return `[${new Date().toISOString()}] ${msg}`;
}
protected write(msg: string): void {
console.log(msg);
}
}
class FileLogger extends Logger {
constructor(private path: string) { super(); }
protected format(msg: string): string {
return `${Date.now()}||${msg}\n`;
}
protected write(msg: string): void {
// In real code: fs.appendFileSync(this.path, msg)
console.log(`Writing to ${this.path}: ${msg}`);
}
}
const logger: Logger = new ConsoleLogger();
logger.log('Application started');Using abstract classes as type annotations
Abstract classes work as types. A variable typed as an abstract class accepts any concrete subclass instance.
abstract class Renderer {
abstract render(content: string): string;
}
class HtmlRenderer extends Renderer {
render(content: string): string {
return `<div>${content}</div>`;
}
}
class MarkdownRenderer extends Renderer {
render(content: string): string {
return content.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
}
}
function renderPage(renderer: Renderer, content: string): void {
console.log(renderer.render(content));
}
renderPage(new HtmlRenderer(), 'Hello world'); // <div>Hello world</div>
renderPage(new MarkdownRenderer(), '**Bold** text'); // <strong>Bold</strong> textCommon mistakes
Trying to instantiate an abstract class directly with
new— always a compile error.Forgetting to implement all abstract methods in a subclass — TypeScript catches this at compile time.
Overusing abstract classes when a plain interface would suffice. If you have no shared implementation, use an interface.
Not calling
super()in the subclass constructor when the abstract class constructor has required parameters.