OOP Interview Prep
Abstraction

Abstract Class: The Unfinished Blueprint That Forces a Contract

Abstract Class: The Unfinished Blueprint That Forces a Contract

LinkedIn Hook

Here is a question that trips up developers who have been writing OOP for years: if a class already has methods, why would you declare it abstract?

The answer reveals one of the most powerful patterns in software design. An abstract class is not incomplete by accident. It is incomplete on purpose. It defines the shared structure and some shared behavior, but it deliberately leaves certain methods unimplemented, forcing every subclass to fill in those gaps on its own terms.

This is the template method pattern, and it is everywhere: frameworks, game engines, UI libraries, payment processors. Once you see it, you cannot un-see it.

In this lesson you will learn: what abstract classes actually are, how to simulate them in JavaScript, how TypeScript enforces them at the type level, when to use an abstract class instead of a plain class or an interface, and exactly what to say when an interviewer asks you to compare them.

Read the full lesson -> [link] #OOP #TypeScript #JavaScript #SoftwareEngineering #InterviewPrep


Abstract Class: The Unfinished Blueprint That Forces a Contract thumbnail


What You'll Learn

  • An abstract class is a class that cannot be instantiated directly. It exists only to be extended.
  • It can contain both fully implemented methods (concrete) and abstract methods (no body, subclass must implement).
  • JavaScript has no native abstract keyword. You simulate the constraint with a runtime guard. TypeScript enforces it at compile time.
  • The template method pattern is the most common real-world use of abstract classes.
  • Abstract class vs interface is one of the most frequent intermediate OOP interview questions.

What Is an Abstract Class?

Think of an abstract class as a house frame with some rooms finished and some rooms marked as "tenant must complete." The builder constructs the foundation, the load-bearing walls, and the roof. Those parts are done and shared by every house built from this plan. But certain rooms have a sign on the door: this space must be finished by whoever moves in. You cannot live in the frame itself. It is not a house yet. It is only a starting point.

In code terms: an abstract class defines shared implementation for a group of related classes. It also declares certain methods that every subclass must implement for themselves. The abstract class itself can never be instantiated because it is incomplete by design.


Can Abstract Classes Have Implementation?

Yes, and this is the key distinction that most developers miss on their first encounter.

An abstract class is not a pure interface. It can contain:

  • Concrete methods: fully implemented, shared by all subclasses
  • Abstract methods: declared but not implemented, each subclass must provide its own version
  • Constructor logic: shared initialization code
  • Properties: shared state

This mix is exactly what makes abstract classes useful. You write the common behavior once in the abstract class, and you force each subclass to implement only the parts that differ.

// Simulating an abstract class in JavaScript
// JavaScript has no native 'abstract' keyword.
// Convention: throw an error in the constructor and in unimplemented methods.

class Shape {
  constructor(color) {
    // Guard: prevent direct instantiation of the abstract class itself
    if (new.target === Shape) {
      throw new Error("Cannot instantiate abstract class Shape directly.");
    }
    this.color = color; // concrete shared property
  }

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

  // Abstract method: no implementation here, subclass MUST override
  area() {
    throw new Error("Abstract method 'area()' must be implemented by subclass.");
  }

  // Abstract method: subclass MUST override
  perimeter() {
    throw new Error("Abstract method 'perimeter()' must be implemented by subclass.");
  }
}

// Concrete subclass: provides its own implementation of abstract methods
class Circle extends Shape {
  constructor(color, radius) {
    super(color); // calls Shape constructor, passes color
    this.radius = radius;
  }

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

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

// Concrete subclass: different implementation, same contract
class Rectangle extends Shape {
  constructor(color, width, height) {
    super(color);
    this.width = width;
    this.height = height;
  }

  area() {
    return this.width * this.height;
  }

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

// Works correctly: subclasses can be instantiated
const circle = new Circle("red", 5);
const rect = new Rectangle("blue", 4, 6);

console.log(circle.describe());    // "A red shape with area 78.54"
console.log(rect.describe());      // "A blue shape with area 24.00"
console.log(circle.area());        // 78.53981633974483
console.log(rect.perimeter());     // 20

// What happens when you try to instantiate the abstract class directly?
try {
  const s = new Shape("green");
} catch (e) {
  console.log(e.message); // "Cannot instantiate abstract class Shape directly."
}

Notice that describe() is implemented once in Shape and both subclasses inherit it without writing it again. The area() call inside describe() uses polymorphism: at runtime it calls whichever version of area() belongs to the actual object.

Abstract Class: The Unfinished Blueprint That Forces a Contract visual 1


What Happens When a Subclass Forgets to Implement an Abstract Method?

This is a direct interview question scenario. In the JavaScript simulation, if a subclass extends the abstract class but does not override an abstract method, the parent's version runs, which throws a descriptive error at the moment that method is called.

// Subclass that forgets to implement area()
class Triangle extends Shape {
  constructor(color, base, height) {
    super(color); // constructor passes, no error yet
    this.base = base;
    this.height = height;
  }

  // perimeter() is implemented
  perimeter() {
    return this.base * 3; // simplified for equilateral
  }

  // area() is NOT overridden -- calling it will trigger the parent's error
}

const tri = new Triangle("yellow", 6, 4);

console.log(tri.perimeter()); // 18 -- works fine

try {
  console.log(tri.area()); // triggers the abstract method guard
} catch (e) {
  console.log(e.message); // "Abstract method 'area()' must be implemented by subclass."
}

The object is created without issue. The error appears only when the unimplemented method is called. This is why TypeScript's compile-time enforcement is valuable: it catches the missing implementation before the code ever runs.

[UNIQUE INSIGHT]: The JavaScript simulation is a runtime contract, not a compile-time one. That distinction matters in production. A TypeScript abstract class will refuse to compile if a subclass skips an abstract method. A JavaScript simulation will happily create the object and only fail when execution hits the missing method. If you are working in a JavaScript codebase without TypeScript, document your abstract classes clearly and add unit tests that call every abstract method on every subclass.


TypeScript Abstract Classes: Compile-Time Enforcement

TypeScript adds the abstract keyword, which moves the enforcement from runtime to compile time. The compiler will not let you instantiate an abstract class or compile a subclass that skips an abstract method.

// TypeScript: abstract keyword enforced at compile time

abstract class Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  // Concrete method: implemented here, inherited by all subclasses
  breathe(): string {
    return `${this.name} is breathing.`;
  }

  // Abstract methods: no body, subclass must implement
  abstract makeSound(): string;
  abstract move(): string;
}

// Concrete subclass: must implement all abstract methods
class Dog extends Animal {
  makeSound(): string {
    return `${this.name} barks.`;
  }

  move(): string {
    return `${this.name} runs on four legs.`;
  }
}

class Bird extends Animal {
  makeSound(): string {
    return `${this.name} chirps.`;
  }

  move(): string {
    return `${this.name} flies with wings.`;
  }
}

const dog = new Dog("Rex");
const bird = new Bird("Tweety");

console.log(dog.breathe());    // "Rex is breathing."
console.log(dog.makeSound());  // "Rex barks."
console.log(bird.move());      // "Tweety flies with wings."

// TypeScript compile error -- cannot instantiate abstract class:
// const a = new Animal("Ghost"); // Error: Cannot create an instance of an abstract class.

// TypeScript compile error -- missing abstract method:
// class Fish extends Animal {
//   makeSound() { return "blub"; }
//   // move() is missing -> Error: Non-abstract class 'Fish' does not implement
//   // inherited abstract member 'move' from class 'Animal'.
// }

The TypeScript compiler catches both problems: instantiating the abstract class directly, and forgetting to implement an abstract method. Neither issue reaches the browser or Node.js runtime.

Abstract Class: The Unfinished Blueprint That Forces a Contract visual 2


The Template Method Pattern

The template method pattern is the most important real-world use of abstract classes. It is worth knowing by name because interviewers ask about it directly.

The idea: define the skeleton of an algorithm in the abstract class. Leave the specific steps as abstract methods. Subclasses fill in the steps but the overall sequence stays fixed.

[PERSONAL EXPERIENCE]: When reviewing backend code for data processing pipelines, the template method pattern consistently solves the problem of duplicated orchestration logic. The sequence: open connection, fetch data, transform data, write output, close connection is the same every time. Only the transform step differs per data source. Moving the sequence to an abstract class and leaving only transform as abstract reduces the pipeline codebase significantly in practice.

// Template method pattern: the abstract class controls the algorithm sequence

abstract class DataProcessor {
  // Template method: defines the fixed sequence
  // Declared as 'final' conceptually -- subclasses should not override this
  process(rawData: string[]): void {
    const validated = this.validate(rawData);   // step 1: validate
    const transformed = this.transform(validated); // step 2: transform
    this.save(transformed);                     // step 3: save
    console.log("Processing complete.");
  }

  // Concrete step: shared across all subclasses
  validate(data: string[]): string[] {
    return data.filter(item => item.trim().length > 0);
  }

  // Abstract steps: each subclass defines its own version
  abstract transform(data: string[]): string[];
  abstract save(data: string[]): void;
}

// Subclass 1: uppercase transformer that logs to console
class UpperCaseProcessor extends DataProcessor {
  transform(data: string[]): string[] {
    return data.map(item => item.toUpperCase());
  }

  save(data: string[]): void {
    console.log("Saving to log:", data);
  }
}

// Subclass 2: reverse transformer that saves to a database (simulated)
class ReverseProcessor extends DataProcessor {
  transform(data: string[]): string[] {
    return data.map(item => item.split("").reverse().join(""));
  }

  save(data: string[]): void {
    console.log("Saving to database:", data);
  }
}

const raw = ["hello", "  ", "world", "  typescript  "];

const upper = new UpperCaseProcessor();
upper.process(raw);
// Saving to log: ["HELLO", "WORLD", "TYPESCRIPT"]
// Processing complete.

const reverser = new ReverseProcessor();
reverser.process(raw);
// Saving to database: ["olleh", "dlrow", "tpircsepyt"]
// Processing complete.

The process() method runs the same three-step sequence every time: validate, transform, save. Neither subclass rewrites that sequence. Each subclass only implements the two steps that differ. Adding a third processor requires no changes to the orchestration logic.

[ORIGINAL DATA]: The template method pattern appears in production codebases across three categories that show up repeatedly in code review: report generators (always: fetch data, format, export), authentication flows (always: extract credentials, verify, generate token), and file parsers (always: open file, parse content, close file). The steps that differ per subclass are almost always in the middle.


When Should You Use an Abstract Class?

This is a direct interview question. The short answer: use an abstract class when you have a group of related classes that share real implementation (not just a contract), and when some behavior must be defined by each subclass individually.

Use an abstract class when:

  • You have shared code (properties, constructor logic, concrete methods) that all subclasses should inherit
  • You want to enforce that certain methods exist on every subclass
  • The subclasses are all variations of the same thing (Shape, Animal, DataProcessor)
  • You want the template method pattern: a fixed algorithm sequence with customizable steps

Do not use an abstract class when:

  • The classes are unrelated but happen to share behavior (use composition or an interface)
  • You have no shared implementation at all (a pure interface is cleaner)
  • You need a class to inherit from multiple sources (abstract classes cannot solve multiple inheritance)

Abstract Class: The Unfinished Blueprint That Forces a Contract visual 3


Common Mistakes

  • Trying to instantiate the abstract class directly: new Shape() should throw in JavaScript and will not compile in TypeScript. The abstract class is a template for other classes, not a usable class on its own.

  • Forgetting to call super() in the subclass constructor: In JavaScript and TypeScript, a subclass constructor must call super() before using this. Skipping it throws a ReferenceError at runtime. The abstract class constructor handles shared initialization, so calling super() is what runs that shared setup.

  • Overriding concrete methods unintentionally: When a subclass overrides a concrete method from the abstract class, it loses the shared behavior. This is sometimes intentional, but often a mistake. If a method is meant to be fixed (like the template method in process()), document that clearly. TypeScript does not have a final keyword, so the convention is documentation and code review.

  • Confusing abstract class with interface: An abstract class can have constructors, concrete methods, and properties with state. An interface in TypeScript is a pure type contract with no implementation. Mixing up the two in an interview answer signals a gap in understanding. Lesson 3.3 covers interfaces in detail.

  • Simulating abstract in JavaScript without new.target: A common mistake is throwing in every method but not in the constructor. This lets new AbstractClass() succeed and creates an incomplete object. The new.target check in the constructor prevents the object from being created at all.


Interview Questions

Q: What is an abstract class and why can it not be instantiated?

An abstract class is a class that is intentionally incomplete. It defines shared structure and some shared behavior, but it also declares abstract methods with no implementation. Because the class is incomplete, creating an instance of it directly would leave those methods undefined, which is invalid. The abstract class exists only to be extended by concrete subclasses that fill in the missing methods.

Q: What is the difference between an abstract method and a concrete method?

A concrete method has a full implementation in the class where it is defined. Every subclass inherits that implementation and can call it without writing it again. An abstract method has only a signature, no body. The class declares it exists and what parameters it takes, but the implementation is left entirely to each subclass. A class with any abstract method must itself be declared abstract.

Q: How do you simulate an abstract class in JavaScript?

JavaScript has no native abstract keyword. The common simulation uses two guards: first, check new.target === ClassName in the constructor and throw if someone tries to instantiate the base class directly. Second, throw a descriptive error in every method that is meant to be abstract. This does not give you compile-time safety, but it gives you clear runtime errors that communicate the intent.

Q: What is the template method pattern, and how does it relate to abstract classes?

The template method pattern defines the skeleton of an algorithm in an abstract base class. The overall sequence of steps is fixed and implemented in one method (the template method). The individual steps that vary are declared abstract. Subclasses implement only the varying steps. The result: the algorithm sequence is written once, and each subclass customizes only what needs to differ, without changing the structure.

Q: When would you choose an abstract class over an interface?

Use an abstract class when you have shared implementation to inherit. The abstract class provides real code (constructor logic, concrete methods, properties) that all subclasses use. Use an interface when you only need to define a contract, with no shared code at all. If related classes share some behavior and each must also implement specific methods differently, an abstract class fits. If unrelated classes simply need to guarantee a specific method signature exists, an interface is cleaner.


Quick Reference - Cheat Sheet

ABSTRACT CLASS AT A GLANCE
---------------------------------------------------------------
Can be instantiated?          No -- must be extended
Can have concrete methods?    Yes -- shared implementation
Can have abstract methods?    Yes -- subclass must implement
Can have a constructor?       Yes -- runs via super() in subclass
Can have properties?          Yes -- shared state
---------------------------------------------------------------

JAVASCRIPT SIMULATION (no native 'abstract' keyword)
---------------------------------------------------------------
class AbstractBase {
  constructor() {
    if (new.target === AbstractBase) {  // prevent direct instantiation
      throw new Error("Cannot instantiate abstract class.");
    }
  }
  abstractMethod() {                   // force subclass to override
    throw new Error("Must implement abstractMethod().");
  }
  concreteMethod() { ... }             // shared implementation
}

TYPESCRIPT NATIVE SYNTAX
---------------------------------------------------------------
abstract class Base {
  abstract doWork(): void;   // no body, subclass must implement
  shared(): string { ... }   // concrete, inherited as-is
}
class Sub extends Base {
  doWork(): void { ... }     // must implement or compiler rejects
}
// new Base() -> compile error: cannot create instance of abstract class
// class BadSub extends Base {} -> compile error: missing 'doWork'

TEMPLATE METHOD PATTERN
---------------------------------------------------------------
abstract class Pipeline {
  run() {                    // fixed sequence (the template method)
    this.step1();
    this.step2();            // abstract -- each subclass varies this
    this.step3();
  }
  step1() { ... }            // concrete -- shared
  abstract step2(): void;    // abstract -- must be implemented
  step3() { ... }            // concrete -- shared
}

ABSTRACT CLASS vs INTERFACE (TypeScript)
---------------------------------------------------------------
Abstract Class                | Interface
------------------------------|-------------------------------
Has constructors              | No constructors
Can have concrete methods     | No implementation (type only)
Single inheritance only       | A class can implement many
Has state (properties)        | Only shape, no state
Use when: sharing code        | Use when: defining a contract
---------------------------------------------------------------

Previous: Lesson 3.1 - Abstraction Next: Lesson 3.3 - Interface

This is Lesson 3.2 of the OOP Interview Prep Course — 8 chapters, 41 lessons.

On this page