TypeScript Abstract Classes
An abstract class is a blueprint that cannot be used directly. It defines a structure — what methods every child class must have — without providing all the details of those methods. Think of it as a half-finished template. The child class fills in the missing parts and becomes the usable, complete version.
The Blueprint Analogy
ARCHITECT'S BLUEPRINT (Abstract Class) ├── Room dimensions → defined (shared rules) ├── Electrical wiring → defined (shared rules) ├── Interior decoration → NOT defined (each owner decides) └── Furniture layout → NOT defined (each owner decides) ACTUAL HOUSE (Child Class) ├── Room dimensions → inherits from blueprint ├── Wiring → inherits from blueprint ├── Interior decoration → owner fills in └── Furniture layout → owner fills in
The blueprint (abstract class) cannot become a house by itself — it needs a builder (child class) to complete it.
Declaring an Abstract Class
The abstract keyword marks a class as abstract. Methods inside it marked with abstract have no body — child classes must provide the body.
abstract class Shape {
// Regular method — shared by all shapes
public describe(): string {
return `I am a ${this.constructor.name}`;
}
// Abstract method — each shape must define its own
abstract getArea(): number;
abstract getPerimeter(): number;
}
// Cannot instantiate directly
// let s = new Shape(); // Error: Cannot create an instance of an abstract class
Implementing Abstract Classes
abstract class Shape {
abstract getArea(): number;
abstract getPerimeter(): number;
public describe(): string {
return `Area: ${this.getArea()}, Perimeter: ${this.getPerimeter()}`;
}
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
getArea(): number {
return Math.PI * this.radius * this.radius;
}
getPerimeter(): number {
return 2 * Math.PI * this.radius;
}
}
class Rectangle extends Shape {
constructor(private width: number, private height: number) {
super();
}
getArea(): number {
return this.width * this.height;
}
getPerimeter(): number {
return 2 * (this.width + this.height);
}
}
let circle = new Circle(7);
let rect = new Rectangle(10, 5);
console.log(circle.describe());
// Area: 153.94, Perimeter: 43.98
console.log(rect.describe());
// Area: 50, Perimeter: 30
How It Works Step by Step
abstract Shape
├── abstract getArea() ← no body, child must add one
├── abstract getPerimeter() ← no body, child must add one
└── describe() ← has body, shared as-is
│
├── Circle (child)
│ ├── getArea() ← radius * radius * π
│ └── getPerimeter() ← 2 * π * radius
│
└── Rectangle (child)
├── getArea() ← width * height
└── getPerimeter() ← 2 * (width + height)
Abstract vs Regular vs Interface
| Feature | Abstract Class | Regular Class | Interface |
|---|---|---|---|
| Can be instantiated | No | Yes | No |
| Can have method bodies | Yes (some) | Yes (all) | No |
| Can have constructors | Yes | Yes | No |
| Can have access modifiers | Yes | Yes | No |
| Forces child to implement methods | Yes | No | Yes |
| Supports multiple inheritance | No (one parent) | No (one parent) | Yes (many interfaces) |
Real-World Example: Payment System
abstract class PaymentGateway {
protected transactionId: string;
constructor(transactionId: string) {
this.transactionId = transactionId;
}
// Shared method — same for every gateway
public logTransaction(): void {
console.log(`Transaction ID: ${this.transactionId} processed.`);
}
// Each gateway handles payment its own way
abstract processPayment(amount: number): string;
abstract refund(amount: number): string;
}
class UPIGateway extends PaymentGateway {
constructor(txId: string, private upiId: string) {
super(txId);
}
processPayment(amount: number): string {
return `UPI ${this.upiId}: ₹${amount} paid. TxID: ${this.transactionId}`;
}
refund(amount: number): string {
return `UPI refund of ₹${amount} to ${this.upiId}`;
}
}
class CardGateway extends PaymentGateway {
constructor(txId: string, private last4Digits: string) {
super(txId);
}
processPayment(amount: number): string {
return `Card ****${this.last4Digits}: ₹${amount} charged.`;
}
refund(amount: number): string {
return `Card refund of ₹${amount} to ****${this.last4Digits}`;
}
}
let upi = new UPIGateway("TX001", "rahul@upi");
let card = new CardGateway("TX002", "4242");
console.log(upi.processPayment(1500));
// UPI rahul@upi: ₹1500 paid. TxID: TX001
console.log(card.processPayment(3200));
// Card ****4242: ₹3200 charged.
upi.logTransaction();
// Transaction ID: TX001 processed.
Abstract Class with Constructor Logic
abstract class Vehicle {
public readonly make: string;
public readonly model: string;
protected fuelLevel: number = 100;
constructor(make: string, model: string) {
this.make = make;
this.model = model;
}
public refuel(): void {
this.fuelLevel = 100;
console.log(`${this.make} ${this.model} refueled.`);
}
abstract startEngine(): string;
abstract getFuelEfficiency(): string;
}
class ElectricCar extends Vehicle {
startEngine(): string {
return `${this.make} ${this.model} starts silently (electric).`;
}
getFuelEfficiency(): string {
return "250 km per charge";
}
}
class PetrolCar extends Vehicle {
startEngine(): string {
return `${this.make} ${this.model} engine roars!`;
}
getFuelEfficiency(): string {
return "15 km per litre";
}
}
let tesla = new ElectricCar("Tesla", "Model 3");
let swift = new PetrolCar("Maruti", "Swift");
console.log(tesla.startEngine());
// Tesla Model 3 starts silently (electric).
console.log(swift.getFuelEfficiency());
// 15 km per litre
swift.refuel();
// Maruti Swift refueled.
Using Abstract Class as a Type
Even though an abstract class cannot be instantiated, it works as a type. A variable typed as the abstract class can hold any of its child class instances.
abstract class Animal {
abstract makeSound(): string;
public sleep(): string {
return `${this.constructor.name} is sleeping.`;
}
}
class Dog extends Animal {
makeSound(): string { return "Woof!"; }
}
class Cat extends Animal {
makeSound(): string { return "Meow!"; }
}
// Animal as a type — holds any subclass
let animals: Animal[] = [new Dog(), new Cat(), new Dog()];
for (let animal of animals) {
console.log(animal.makeSound());
}
// Woof!
// Meow!
// Woof!
Key Rules
| Rule | Detail |
|---|---|
Use abstract keyword | Before the class keyword and before method names |
| No instantiation | new AbstractClass() gives a compile error |
| Child class must implement all abstract methods | Missing any one gives a compile error |
| Non-abstract methods are inherited normally | Child classes get them without re-defining |
| Can be used as a type | Variables and arrays can have the abstract class as their type |
