Abstract Classes & Implements

Difficulty: Intermediate

Question

What are abstract classes in TypeScript? How do they differ from interfaces and when should you use each?

Answer

Abstract classes are base classes that cannot be instantiated directly. They can contain both implemented methods (shared logic) and abstract methods (contracts subclasses must fulfill).

Key differences from interfaces: - Abstract classes can have implementation (method bodies, property initializers) - Abstract classes can have constructors, access modifiers, and static members - A class can only extend ONE abstract class but implement MANY interfaces - Abstract classes produce runtime JavaScript code; interfaces are erased

Use abstract classes when you have shared implementation logic. Use interfaces when you only need a contract with no shared code.

Code examples

Abstract Class Basics

abstract class Shape {
  constructor(public color: string) {}

  // Abstract method - must be implemented by subclasses
  abstract area(): number;
  abstract perimeter(): number;

  // Concrete method - shared implementation
  describe(): string {
    return `A ${this.color} shape with area ${this.area().toFixed(2)}`;
  }
}

class Circle extends Shape {
  constructor(color: string, public radius: number) {
    super(color);
  }

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

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

class Rectangle extends Shape {
  constructor(color: string, public width: number, public height: number) {
    super(color);
  }

  area(): number { return this.width * this.height; }
  perimeter(): number { return 2 * (this.width + this.height); }
}

// const s = new Shape('red'); // Error: Cannot create instance of abstract class
const c = new Circle('red', 5);
c.describe(); // 'A red shape with area 78.54'

Abstract classes combine contract enforcement (abstract methods) with code reuse (concrete methods). Subclasses must implement all abstract members.

Abstract vs Interface

// Interface - pure contract, no implementation
interface Logger {
  log(message: string): void;
  error(message: string): void;
  warn(message: string): void;
}

// Abstract class - contract + shared implementation
abstract class BaseLogger {
  abstract log(message: string): void;

  // Shared implementation
  error(message: string): void {
    this.log(`[ERROR] ${message}`);
  }

  warn(message: string): void {
    this.log(`[WARN] ${message}`);
  }

  // Abstract property
  abstract get prefix(): string;
}

class ConsoleLogger extends BaseLogger {
  get prefix() { return 'Console'; }

  log(message: string): void {
    console.log(`[${this.prefix}] ${message}`);
  }
}

class FileLogger extends BaseLogger {
  get prefix() { return 'File'; }

  log(message: string): void {
    // fs.appendFileSync('app.log', message + '\n');
  }
}

// Both can be used polymorphically
function useLogger(logger: BaseLogger) {
  logger.log('Starting app');
  logger.warn('Low memory');
  logger.error('Crash!');
}

The abstract BaseLogger provides error() and warn() implementations. Subclasses only need to implement log(). An interface would force every class to implement all three methods.

Template Method Pattern

// Abstract class as a template method pattern
abstract class DataProcessor<T, R> {
  // Template method - defines the algorithm skeleton
  process(input: T): R {
    this.validate(input);
    const transformed = this.transform(input);
    return this.format(transformed);
  }

  // Hooks for subclasses to implement
  abstract validate(input: T): void;
  abstract transform(input: T): unknown;
  abstract format(data: unknown): R;
}

class CSVProcessor extends DataProcessor<string, string[][]> {
  validate(input: string): void {
    if (!input.includes(',')) throw new Error('Not valid CSV');
  }

  transform(input: string): string[][] {
    return input.split('\n').map(row => row.split(','));
  }

  format(data: unknown): string[][] {
    return data as string[][];
  }
}

class JSONProcessor extends DataProcessor<string, object> {
  validate(input: string): void {
    try { JSON.parse(input); } catch { throw new Error('Invalid JSON'); }
  }

  transform(input: string): object {
    return JSON.parse(input);
  }

  format(data: unknown): object {
    return data as object;
  }
}

The Template Method pattern is a classic use case for abstract classes. The base class defines the algorithm, subclasses provide the steps.

Key points

Concepts covered

Abstract Classes, Abstract Methods, Inheritance, Polymorphism, Interface vs Abstract