Abstract classes and interfaces are powerful tools for defining contracts in TypeScript. Abstract classes provide a base with both abstract and concrete members, while interfaces define pure contracts that classes must implement. Understanding when to use each is crucial for building well-architected applications. Let's master abstraction!
Abstract Classes
Abstract classes are base classes that cannot be instantiated directly. They can contain both abstract methods (without implementation) and concrete methods (with implementation).
// Abstract class - cannot be instantiated
abstract class Shape {
constructor(public name: string) {}
// Abstract method - must be implemented by subclasses
abstract getArea(): number;
abstract getPerimeter(): number;
// Concrete method - available to all subclasses
describe(): void {
console.log(`\nShape: ${this.name}`);
console.log(`Area: ${this.getArea()}`);
console.log(`Perimeter: ${this.getPerimeter()}`);
}
// Another concrete method
compareArea(other: Shape): void {
const thisArea = this.getArea();
const otherArea = other.getArea();
if (thisArea > otherArea) {
console.log(`${this.name} is larger than ${other.name}`);
} else if (thisArea < otherArea) {
console.log(`${this.name} is smaller than ${other.name}`);
} else {
console.log(`${this.name} and ${other.name} have the same area`);
}
}
}
// Concrete class extending abstract class
class Circle extends Shape {
constructor(public radius: number) {
super("Circle");
}
// Must implement abstract methods
getArea(): number {
return Math.PI * this.radius ** 2;
}
getPerimeter(): number {
return 2 * Math.PI * this.radius;
}
}
class Rectangle extends Shape {
constructor(public width: number, public height: number) {
super("Rectangle");
}
getArea(): number {
return this.width * this.height;
}
getPerimeter(): number {
return 2 * (this.width + this.height);
}
}
// Usage
const circle = new Circle(5);
const rectangle = new Rectangle(10, 20);
circle.describe();
// Shape: Circle
// Area: 78.53981633974483
// Perimeter: 31.41592653589793
rectangle.describe();
// Shape: Rectangle
// Area: 200
// Perimeter: 60
circle.compareArea(rectangle);
// Circle is smaller than Rectangle
// ❌ Cannot instantiate abstract class
// const shape = new Shape("Generic"); // Error!💡 Abstract Classes vs Regular Classes
Use abstract when you want to create a base class that should never be instantiated directly. Abstract classes are perfect for defining common functionality while requiring subclasses to implement specific behaviors.
Abstract Methods and Properties
abstract class Employee {
constructor(
public name: string,
protected baseSalary: number
) {}
// Abstract methods - no implementation
abstract calculateSalary(): number;
abstract getJobTitle(): string;
// Abstract property (getter)
abstract get department(): string;
// Concrete method using abstract methods
displayInfo(): void {
console.log(`\nEmployee: ${this.name}`);
console.log(`Title: ${this.getJobTitle()}`);
console.log(`Department: ${this.department}`);
console.log(`Salary: ₦${this.calculateSalary().toLocaleString()}`);
}
// Protected method for subclasses
protected getBaseSalary(): number {
return this.baseSalary;
}
}
class Developer extends Employee {
private readonly dept = "Engineering";
constructor(
name: string,
baseSalary: number,
private programmingLanguages: string[],
private yearsExperience: number
) {
super(name, baseSalary);
}
// Implement abstract methods
calculateSalary(): number {
const experienceBonus = this.yearsExperience * 50000;
const languageBonus = this.programmingLanguages.length * 25000;
return this.baseSalary + experienceBonus + languageBonus;
}
getJobTitle(): string {
if (this.yearsExperience >= 5) {
return "Senior Developer";
} else if (this.yearsExperience >= 2) {
return "Mid-level Developer";
}
return "Junior Developer";
}
// Implement abstract property
get department(): string {
return this.dept;
}
// Additional developer-specific method
listSkills(): void {
console.log(`Languages: ${this.programmingLanguages.join(", ")}`);
}
}
class Manager extends Employee {
private readonly dept = "Management";
constructor(
name: string,
baseSalary: number,
private teamSize: number,
private bonusPercentage: number
) {
super(name, baseSalary);
}
calculateSalary(): number {
const teamBonus = this.teamSize * 30000;
const performanceBonus = this.baseSalary * (this.bonusPercentage / 100);
return this.baseSalary + teamBonus + performanceBonus;
}
getJobTitle(): string {
if (this.teamSize >= 10) {
return "Senior Manager";
}
return "Manager";
}
get department(): string {
return this.dept;
}
getTeamSize(): number {
return this.teamSize;
}
}
// Create employees
const developer = new Developer(
"Yusuf Ibrahim",
500000,
["TypeScript", "React", "Node.js"],
3
);
const manager = new Manager(
"Amina Hassan",
800000,
12,
15
);
// Use polymorphically
const employees: Employee[] = [developer, manager];
employees.forEach(emp => emp.displayInfo());
// Employee: Yusuf Ibrahim
// Title: Mid-level Developer
// Department: Engineering
// Salary: ₦725,000
// Employee: Amina Hassan
// Title: Senior Manager
// Department: Management
// Salary: ₦1,480,000
developer.listSkills();
// Languages: TypeScript, React, Node.jsInterfaces
Interfaces define contracts that classes must follow. Unlike abstract classes, interfaces only define structure—no implementation.
// Interface defines a contract
interface Printable {
print(): void;
}
interface Saveable {
save(): void;
load(): void;
}
// Class implementing single interface
class Document implements Printable {
constructor(public content: string) {}
print(): void {
console.log("Printing document:");
console.log(this.content);
}
}
// Class implementing multiple interfaces
class TextFile implements Printable, Saveable {
constructor(
public filename: string,
public content: string
) {}
print(): void {
console.log(`Printing ${this.filename}:`);
console.log(this.content);
}
save(): void {
console.log(`Saving ${this.filename}...`);
console.log("File saved successfully");
}
load(): void {
console.log(`Loading ${this.filename}...`);
console.log("File loaded successfully");
}
}
// Using interfaces
const doc = new Document("Hello, TypeScript!");
doc.print();
const file = new TextFile("document.txt", "TypeScript is awesome!");
file.print();
file.save();
file.load();
// Interface with properties
interface User {
id: number;
name: string;
email: string;
getFullInfo(): string;
}
class RegisteredUser implements User {
constructor(
public id: number,
public name: string,
public email: string,
public registrationDate: Date
) {}
getFullInfo(): string {
return `${this.name} (${this.email}) - ID: ${this.id}`;
}
daysSinceRegistration(): number {
const now = new Date();
const diff = now.getTime() - this.registrationDate.getTime();
return Math.floor(diff / (1000 * 60 * 60 * 24));
}
}
const user = new RegisteredUser(
1,
"Yusuf Ibrahim",
"yusuf@example.com",
new Date("2024-01-01")
);
console.log(user.getFullInfo());
console.log(`Days since registration: ${user.daysSinceRegistration()}`);The implements Keyword
// Multiple interfaces
interface Flyable {
fly(): void;
altitude: number;
}
interface Swimmable {
swim(): void;
depth: number;
}
interface Walkable {
walk(): void;
speed: number;
}
// Class implementing multiple interfaces
class Duck implements Flyable, Swimmable, Walkable {
altitude: number = 0;
depth: number = 0;
speed: number = 5;
constructor(public name: string) {}
fly(): void {
this.altitude = 100;
console.log(`${this.name} is flying at ${this.altitude}m`);
}
swim(): void {
this.depth = 2;
console.log(`${this.name} is swimming at ${this.depth}m depth`);
}
walk(): void {
console.log(`${this.name} is walking at ${this.speed}km/h`);
}
}
const duck = new Duck("Donald");
duck.fly(); // "Donald is flying at 100m"
duck.swim(); // "Donald is swimming at 2m depth"
duck.walk(); // "Donald is walking at 5km/h"
// Interface with optional properties
interface Config {
host: string;
port: number;
timeout?: number;
retries?: number;
}
class ServerConfig implements Config {
host: string;
port: number;
timeout?: number;
retries?: number;
constructor(host: string, port: number) {
this.host = host;
this.port = port;
this.timeout = 30000; // Optional, but providing default
this.retries = 3;
}
displayConfig(): void {
console.log(`Server: ${this.host}:${this.port}`);
console.log(`Timeout: ${this.timeout}ms`);
console.log(`Retries: ${this.retries}`);
}
}
const config = new ServerConfig("localhost", 8080);
config.displayConfig();
// Interface extending interface
interface Animal {
name: string;
makeSound(): void;
}
interface Pet extends Animal {
owner: string;
play(): void;
}
class Dog implements Pet {
constructor(
public name: string,
public owner: string,
public breed: string
) {}
makeSound(): void {
console.log(`${this.name} says: Woof!`);
}
play(): void {
console.log(`${this.name} is playing with ${this.owner}`);
}
}
const dog = new Dog("Max", "Yusuf", "Golden Retriever");
dog.makeSound(); // "Max says: Woof!"
dog.play(); // "Max is playing with Yusuf"Abstract Class vs Interface
| Feature | Abstract Class | Interface |
|---|---|---|
| Implementation | Can have concrete methods | Only declarations (no implementation) |
| Multiple inheritance | Only one abstract class | Multiple interfaces allowed |
| Access modifiers | public, private, protected | All members are public |
| Constructors | Can have constructors | Cannot have constructors |
| Keywords | extends (single) | implements (multiple) |
| Use case | Shared code + contract | Pure contract only |
// When to use Abstract Class: Shared implementation
abstract class PaymentProcessor {
constructor(protected transactionId: string) {}
// Concrete method - shared implementation
logTransaction(): void {
console.log(`Transaction ${this.transactionId} logged`);
}
// Abstract method - must be implemented
abstract processPayment(amount: number): boolean;
abstract getProcessorName(): string;
}
class CreditCardProcessor extends PaymentProcessor {
processPayment(amount: number): boolean {
console.log(`Processing ₦${amount} via credit card`);
this.logTransaction(); // Use inherited method
return true;
}
getProcessorName(): string {
return "Credit Card Processor";
}
}
// When to use Interface: Pure contract
interface Logger {
log(message: string): void;
error(message: string): void;
}
interface Storage {
save(key: string, value: any): void;
load(key: string): any;
}
// Class implementing multiple interfaces
class FileLogger implements Logger, Storage {
log(message: string): void {
console.log(`[LOG] ${message}`);
}
error(message: string): void {
console.error(`[ERROR] ${message}`);
}
save(key: string, value: any): void {
console.log(`Saving ${key}: ${value}`);
}
load(key: string): any {
console.log(`Loading ${key}`);
return null;
}
}
// Can implement interface AND extend abstract class
abstract class BaseService {
protected initialized: boolean = false;
initialize(): void {
this.initialized = true;
console.log("Service initialized");
}
}
interface Cacheable {
cache: Map<string, any>;
cacheResult(key: string, value: any): void;
getCached(key: string): any;
}
class DataService extends BaseService implements Cacheable {
cache = new Map<string, any>();
cacheResult(key: string, value: any): void {
this.cache.set(key, value);
}
getCached(key: string): any {
return this.cache.get(key);
}
}
const service = new DataService();
service.initialize(); // From abstract class
service.cacheResult("key1", "value1"); // From interface
console.log(service.getCached("key1")); // "value1"Practical Examples
Example 1: Database Abstraction Layer
// Abstract base class for database operations
abstract class Database {
protected isConnected: boolean = false;
constructor(protected connectionString: string) {}
// Abstract methods - each database implements differently
abstract connect(): Promise<void>;
abstract disconnect(): Promise<void>;
abstract query(sql: string): Promise<any>;
abstract execute(sql: string): Promise<boolean>;
// Concrete helper method
protected log(message: string): void {
console.log(`[${new Date().toISOString()}] ${message}`);
}
// Concrete method using abstract methods
async executeTransaction(queries: string[]): Promise<boolean> {
if (!this.isConnected) {
await this.connect();
}
this.log("Starting transaction");
try {
for (const sql of queries) {
await this.execute(sql);
}
this.log("Transaction committed");
return true;
} catch (error) {
this.log(`Transaction failed: ${error}`);
return false;
}
}
}
// MySQL implementation
class MySQLDatabase extends Database {
async connect(): Promise<void> {
this.log(`Connecting to MySQL: ${this.connectionString}`);
// Simulate connection
await new Promise(resolve => setTimeout(resolve, 100));
this.isConnected = true;
this.log("MySQL connected");
}
async disconnect(): Promise<void> {
this.log("Disconnecting from MySQL");
this.isConnected = false;
}
async query(sql: string): Promise<any> {
this.log(`MySQL Query: ${sql}`);
return { rows: [] };
}
async execute(sql: string): Promise<boolean> {
this.log(`MySQL Execute: ${sql}`);
return true;
}
}
// PostgreSQL implementation
class PostgreSQLDatabase extends Database {
async connect(): Promise<void> {
this.log(`Connecting to PostgreSQL: ${this.connectionString}`);
await new Promise(resolve => setTimeout(resolve, 100));
this.isConnected = true;
this.log("PostgreSQL connected");
}
async disconnect(): Promise<void> {
this.log("Disconnecting from PostgreSQL");
this.isConnected = false;
}
async query(sql: string): Promise<any> {
this.log(`PostgreSQL Query: ${sql}`);
return { rows: [] };
}
async execute(sql: string): Promise<boolean> {
this.log(`PostgreSQL Execute: ${sql}`);
return true;
}
}
// Usage - polymorphic database operations
async function performDatabaseOperations(db: Database) {
await db.connect();
const result = await db.query("SELECT * FROM users");
console.log("Query result:", result);
const success = await db.executeTransaction([
"INSERT INTO users (name) VALUES ('Yusuf')",
"INSERT INTO users (name) VALUES ('Amina')"
]);
console.log("Transaction success:", success);
await db.disconnect();
}
// Works with any database implementation
const mysql = new MySQLDatabase("mysql://localhost:3306/myapp");
const postgres = new PostgreSQLDatabase("postgresql://localhost:5432/myapp");
performDatabaseOperations(mysql);
performDatabaseOperations(postgres);Example 2: Plugin System
// Interface for plugin contract
interface Plugin {
name: string;
version: string;
initialize(): void;
execute(context: any): void;
cleanup(): void;
}
// Abstract base for common plugin functionality
abstract class BasePlugin implements Plugin {
protected enabled: boolean = false;
constructor(
public name: string,
public version: string
) {}
initialize(): void {
this.enabled = true;
console.log(`Plugin "${this.name}" v${this.version} initialized`);
}
cleanup(): void {
this.enabled = false;
console.log(`Plugin "${this.name}" cleaned up`);
}
// Abstract - each plugin implements its own logic
abstract execute(context: any): void;
// Helper method
protected log(message: string): void {
console.log(`[${this.name}] ${message}`);
}
}
// Concrete plugin implementations
class AuthenticationPlugin extends BasePlugin {
constructor() {
super("Authentication", "1.0.0");
}
execute(context: any): void {
if (!this.enabled) {
throw new Error("Plugin not initialized");
}
this.log("Authenticating user...");
if (context.username && context.password) {
this.log(`User ${context.username} authenticated`);
context.authenticated = true;
} else {
this.log("Authentication failed");
context.authenticated = false;
}
}
}
class LoggingPlugin extends BasePlugin {
private logs: string[] = [];
constructor() {
super("Logging", "2.0.0");
}
execute(context: any): void {
if (!this.enabled) return;
const logEntry = `[${new Date().toISOString()}] ${JSON.stringify(context)}`;
this.logs.push(logEntry);
this.log(`Logged: ${logEntry}`);
}
getLogs(): string[] {
return [...this.logs];
}
}
class CachePlugin extends BasePlugin {
private cache = new Map<string, any>();
constructor() {
super("Cache", "1.5.0");
}
execute(context: any): void {
if (!this.enabled) return;
if (context.action === "get") {
const cached = this.cache.get(context.key);
this.log(`Cache ${cached ? "hit" : "miss"} for key: ${context.key}`);
context.result = cached;
} else if (context.action === "set") {
this.cache.set(context.key, context.value);
this.log(`Cached ${context.key}`);
}
}
}
// Plugin manager
class PluginManager {
private plugins: Plugin[] = [];
register(plugin: Plugin): void {
plugin.initialize();
this.plugins.push(plugin);
console.log(`Registered plugin: ${plugin.name}`);
}
executeAll(context: any): void {
console.log(`\nExecuting ${this.plugins.length} plugins...`);
this.plugins.forEach(plugin => plugin.execute(context));
}
cleanup(): void {
console.log("\nCleaning up plugins...");
this.plugins.forEach(plugin => plugin.cleanup());
this.plugins = [];
}
}
// Usage
const manager = new PluginManager();
manager.register(new AuthenticationPlugin());
manager.register(new LoggingPlugin());
manager.register(new CachePlugin());
// Execute plugins with different contexts
manager.executeAll({
username: "yusuf",
password: "secret123"
});
manager.executeAll({
action: "set",
key: "user:1",
value: { name: "Yusuf" }
});
manager.executeAll({
action: "get",
key: "user:1"
});
manager.cleanup();Test Your Knowledge
Type Safety Check
Which abstract class usage is correct?
// Option A
abstract class Shape {
abstract getArea(): number;
describe(): void {
console.log(`Area: ${this.getArea()}`);
}
}
class Circle extends Shape {
constructor(public radius: number) {
super();
}
getArea(): number {
return Math.PI * this.radius ** 2;
}
}
const circle = new Circle(5);
// Option B
abstract class Animal {
abstract makeSound(): void;
}
const animal = new Animal(); // Try to instantiate
// Option C
abstract class Vehicle {
abstract start(): void;
}
class Car extends Vehicle {
// Missing start() implementation
}
const car = new Car();
// Option D
interface Drawable {
draw(): void;
}
class Rectangle implements Drawable {
draw(): void {
console.log("Drawing rectangle");
}
}
const rect = new Rectangle();Common TypeScript Error
// Trying to instantiate abstract class
abstract class Database {
abstract connect(): void;
abstract query(sql: string): any;
disconnect(): void {
console.log("Disconnected");
}
}
// Error: Cannot create an instance of an abstract class
const db = new Database(); // ❌ Error!
// Another error: Not implementing abstract methods
class MySQLDatabase extends Database {
connect(): void {
console.log("Connected to MySQL");
}
// Missing query() implementation!
}
const mysql = new MySQLDatabase(); // ❌ Error!❌ Cannot create an instance of an abstract class. Non-abstract class 'MySQLDatabase' does not implement inherited abstract member 'query' from class 'Database'.
What's Wrong?
Abstract classes cannot be instantiated directly—they're templates for other classes. Additionally, all abstract methods must be implemented in concrete subclasses.
abstract class Database {
abstract connect(): void;
abstract query(sql: string): any;
disconnect(): void {
console.log("Disconnected");
}
}
// ✅ Concrete class implementing all abstract methods
class MySQLDatabase extends Database {
connect(): void {
console.log("Connected to MySQL");
}
query(sql: string): any {
console.log(`Executing: ${sql}`);
return { rows: [] };
}
}
// ✅ Now can instantiate the concrete class
const mysql = new MySQLDatabase();
mysql.connect(); // "Connected to MySQL"
mysql.query("SELECT *"); // "Executing: SELECT *"
mysql.disconnect(); // "Disconnected"
// Another implementation
class PostgreSQLDatabase extends Database {
connect(): void {
console.log("Connected to PostgreSQL");
}
query(sql: string): any {
console.log(`PostgreSQL query: ${sql}`);
return { rows: [] };
}
}
const postgres = new PostgreSQLDatabase();
postgres.connect(); // "Connected to PostgreSQL"Solution: Extend the abstract class and implement all abstract methods
Best Practices
- Use abstract classes for shared implementation - when subclasses need common code
- Use interfaces for pure contracts - when only structure matters
- Implement multiple interfaces - but extend only one abstract class
- Make abstract methods meaningful - require essential behavior
- Provide concrete helper methods in abstract classes
- Document abstract classes - explain what subclasses must do
- Use protected members in abstract classes for subclass access
- Prefer composition over inheritance when possible
// ✅ Good: Abstract class with shared implementation
abstract class Repository<T> {
protected items: T[] = [];
// Concrete method - shared by all repositories
getAll(): T[] {
return [...this.items];
}
// Abstract - each repository implements differently
abstract findById(id: number): T | undefined;
abstract save(item: T): void;
}
// ✅ Good: Interface for contract
interface Serializable {
toJSON(): string;
fromJSON(json: string): void;
}
// ✅ Good: Class implementing interface
class User implements Serializable {
constructor(public name: string, public email: string) {}
toJSON(): string {
return JSON.stringify({ name: this.name, email: this.email });
}
fromJSON(json: string): void {
const data = JSON.parse(json);
this.name = data.name;
this.email = data.email;
}
}
// ✅ Good: Multiple interfaces
interface Loggable {
log(): void;
}
interface Cacheable {
cache(): void;
}
class DataService implements Loggable, Cacheable {
log(): void {
console.log("Logging...");
}
cache(): void {
console.log("Caching...");
}
}
// ❌ Avoid: Abstract class for simple contract
abstract class BadExample {
abstract method1(): void;
abstract method2(): void;
// No shared implementation - should be interface!
}
// ✅ Better: Use interface
interface GoodExample {
method1(): void;
method2(): void;
}
// ❌ Avoid: Too many abstract methods without shared code
abstract class TooAbstract {
abstract method1(): void;
abstract method2(): void;
abstract method3(): void;
abstract method4(): void;
abstract method5(): void;
// No concrete methods - should be interface!
}Key Takeaways
- Abstract classes cannot be instantiated—only extended
- Abstract methods must be implemented in concrete subclasses
- Abstract classes can have both abstract and concrete members
- Interfaces define pure contracts with no implementation
- Classes use
implementsfor interfaces,extendsfor abstract classes - A class can implement multiple interfaces
- A class can only extend one abstract class
- Use abstract classes when you need shared implementation
- Use interfaces when you only need structure definition
- Both enable polymorphism and enforce contracts
What's Next?
Congratulations! You've completed the Classes & OOP category and mastered abstract classes and interfaces. You now have a comprehensive understanding of TypeScript's object-oriented programming features—from basic classes to advanced abstraction patterns.
Continue building on this foundation by exploring more advanced TypeScript patterns, decorators, and real-world applications. You're well-equipped to build robust, type-safe applications!