OOP Interview Prep
Abstraction

Abstract Class vs Interface

Knowing When to Use Which

LinkedIn Hook

Here's the question that separates junior developers from mid-level ones in OOP interviews:

"What's the difference between an abstract class and an interface — and when would you pick one over the other?"

Most candidates can give a surface-level answer. Very few can explain the decision clearly with a concrete reason.

The trap isn't knowing the definitions. You probably know them. The trap is this follow-up: "Your abstract class has zero abstract methods and no state. Why not just use an interface?"

If that question made you pause, this lesson is for you.

Read the full lesson → [link]

#OOP #TypeScript #SoftwareEngineering #InterviewPrep #AbstractClass


Abstract Class vs Interface thumbnail


What You'll Learn

  • The precise differences between abstract classes and interfaces in TypeScript
  • A comprehensive comparison table covering implementation, inheritance, constructors, access modifiers, and use cases
  • A decision flowchart to pick the right one on the spot
  • How multiple inheritance works with interfaces but not with abstract classes
  • The interview trap questions that catch candidates who only know definitions

The Analogy That Makes It Click

Think about two things: a job contract and a job training manual.

A job contract tells you what you must do. It lists responsibilities: "you will handle customer calls, you will write reports, you will attend weekly meetings." It says nothing about how to do any of those things. It only defines the obligations. That is an interface.

A training manual is different. It gives you some complete procedures ("to file a report, open the portal, click Submit, wait for confirmation") and leaves others blank for you to fill in based on your department ("how to handle customer escalations: depends on your team's policy"). It has partial implementation and partial blank space. That is an abstract class.

The contract (interface) is a pure promise. The manual (abstract class) is a partial guide that forces you to complete the missing parts yourself.

[INTERNAL-LINK: what abstraction means → Lesson 3.1: Abstraction]


What Is the Core Difference?

An abstract class is a class that can hold both implemented methods and abstract (unimplemented) methods. It can store state, define constructors, and use access modifiers. A class can only extend one abstract class.

An interface is a pure contract. It defines method and property signatures only. It holds no implementation, no state, no constructor. A class can implement as many interfaces as it needs.

The critical distinction is shared state vs shared contract. When multiple related classes need to share actual code and data, use an abstract class. When unrelated classes need to commit to the same behavioral contract, use an interface.

Abstract Class vs Interface visual 1


Comparison Table — Abstract Class vs Interface

ABSTRACT CLASS vs INTERFACE — FULL COMPARISON
-----------------------------------------------------------------------
Feature               Abstract Class          Interface
-----------------------------------------------------------------------
Can have              Yes — fully             No — signatures
implemented methods   implemented methods     only

Can store state       Yes — instance fields   No — no fields
(instance fields)     allowed                 (TypeScript: readonly
                                              const-like allowed)

Constructor           Yes — can define        No — no constructor
                      a constructor

Access modifiers      Yes — public,           No — all members are
                      protected, private      implicitly public

Multiple              No — a class can        Yes — a class can
inheritance           extend only one         implement many
                      abstract class          interfaces

Instantiation         No — cannot create      No — cannot create
                      instances directly      instances directly

Abstract methods      Yes — forces subclass   All methods are
                      to implement            abstract by definition

Can extend/           Can extend one class,   Can extend multiple
implement others      implement interfaces     interfaces

Best for              Shared base with        Pure behavioral
                      partial implementation  contract across
                      + forced overrides      unrelated classes

Example use           Shape with              Serializable,
                      calculateArea()         Printable,
                      abstract, but           Comparable, Flyable
                      shared color +
                      move() implementation
-----------------------------------------------------------------------

[INTERNAL-LINK: abstract methods and the template method pattern → Lesson 3.2: Abstract Class]


Code Example 1 — Abstract Class with Shared State and Partial Implementation

// Abstract class: holds shared state, partial implementation, and forced overrides
abstract class Vehicle {
  // Shared state — all vehicles have these fields
  protected brand: string;
  protected speed: number;

  constructor(brand: string) {
    this.brand = brand;
    this.speed = 0;
  }

  // Concrete method — shared by all subclasses, no override required
  accelerate(amount: number): void {
    this.speed += amount;
    console.log(`${this.brand} accelerating. Speed: ${this.speed} km/h`);
  }

  // Concrete method — shared behavior
  getBrand(): string {
    return this.brand;
  }

  // Abstract method — every vehicle type MUST define its own fuel type
  abstract getFuelType(): string;

  // Abstract method — every vehicle type MUST define its own max speed
  abstract getMaxSpeed(): number;
}

class ElectricCar extends Vehicle {
  private batteryLevel: number;

  constructor(brand: string, batteryLevel: number) {
    super(brand); // calls the abstract class constructor
    this.batteryLevel = batteryLevel;
  }

  // Must implement all abstract methods
  getFuelType(): string {
    return "Electric";
  }

  getMaxSpeed(): number {
    return 250;
  }

  getBatteryStatus(): string {
    return `Battery: ${this.batteryLevel}%`;
  }
}

class GasCar extends Vehicle {
  constructor(brand: string) {
    super(brand);
  }

  getFuelType(): string {
    return "Gasoline";
  }

  getMaxSpeed(): number {
    return 200;
  }
}

const tesla = new ElectricCar("Tesla", 85);
const bmw = new GasCar("BMW");

tesla.accelerate(60);            // Output: Tesla accelerating. Speed: 60 km/h
console.log(tesla.getFuelType()); // Output: Electric
console.log(tesla.getBatteryStatus()); // Output: Battery: 85%

bmw.accelerate(40);              // Output: BMW accelerating. Speed: 40 km/h
console.log(bmw.getFuelType());  // Output: Gasoline

// Cannot instantiate the abstract class directly
// const v = new Vehicle("Generic"); // Error: Cannot create an instance of an abstract class

Notice that accelerate() and getBrand() are implemented once in Vehicle and inherited by both ElectricCar and GasCar. Neither subclass needs to rewrite that logic. This is the key benefit of an abstract class: shared implementation alongside forced overrides.

[PERSONAL EXPERIENCE]: In production codebases, the most common misuse of abstract class is creating one with zero shared state and zero concrete methods. If everything in your abstract class is abstract, you've written an interface using the wrong tool.


Code Example 2 — Interface as a Pure Behavioral Contract

// Interface: a pure contract — no implementation, no state, no constructor
interface Serializable {
  serialize(): string;
  deserialize(data: string): void;
}

interface Printable {
  print(): void;
}

// A class can implement multiple interfaces simultaneously
// This is the multiple inheritance alternative in OOP
class UserProfile implements Serializable, Printable {
  private name: string;
  private email: string;

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

  // Must implement everything from Serializable
  serialize(): string {
    return JSON.stringify({ name: this.name, email: this.email });
  }

  deserialize(data: string): void {
    const parsed = JSON.parse(data);
    this.name = parsed.name;
    this.email = parsed.email;
  }

  // Must implement everything from Printable
  print(): void {
    console.log(`User: ${this.name} | Email: ${this.email}`);
  }
}

class ProductListing implements Serializable, Printable {
  private title: string;
  private price: number;

  constructor(title: string, price: number) {
    this.title = title;
    this.price = price;
  }

  serialize(): string {
    return JSON.stringify({ title: this.title, price: this.price });
  }

  deserialize(data: string): void {
    const parsed = JSON.parse(data);
    this.title = parsed.title;
    this.price = parsed.price;
  }

  print(): void {
    console.log(`Product: ${this.title} | Price: $${this.price}`);
  }
}

const user = new UserProfile("Alice", "alice@example.com");
const product = new ProductListing("Laptop", 999);

user.print();           // Output: User: Alice | Email: alice@example.com
console.log(user.serialize()); // Output: {"name":"Alice","email":"alice@example.com"}

product.print();        // Output: Product: Laptop | Price: $999

// Polymorphism through interfaces — treat different types uniformly
const printables: Printable[] = [user, product];
printables.forEach(item => item.print());
// Output:
// User: Alice | Email: alice@example.com
// Product: Laptop | Price: $999

UserProfile and ProductListing have nothing in common structurally. They share no state, no base behavior. But both commit to the Serializable and Printable contracts. This is exactly the use case for interfaces. You wouldn't put these two under the same abstract class because they don't share a "kind of thing" relationship.


Code Example 3 — The Wrong Way and the Right Way

This is the code pattern that causes most interview confusion. Candidates often reach for the wrong tool.

// WRONG: Using abstract class when there is zero shared state or implementation
// This is an interface disguised as an abstract class
abstract class Logger {
  abstract log(message: string): void;
  abstract warn(message: string): void;
  abstract error(message: string): void;
}
// Problem: no state, no constructor used, no concrete methods
// This adds inheritance coupling with no benefit over an interface

// RIGHT: Use an interface for a pure contract
interface Logger {
  log(message: string): void;
  warn(message: string): void;
  error(message: string): void;
}

// Now ConsoleLogger and FileLogger are completely independent
class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log(`[LOG] ${message}`);
  }

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

  error(message: string): void {
    console.error(`[ERROR] ${message}`);
  }
}

class FileLogger implements Logger {
  private logFile: string;

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

  log(message: string): void {
    // Write to file in production
    console.log(`[FILE:${this.logFile}] LOG: ${message}`);
  }

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

  error(message: string): void {
    console.log(`[FILE:${this.logFile}] ERROR: ${message}`);
  }
}

// Both loggers work anywhere a Logger is expected
function runApplication(logger: Logger): void {
  logger.log("Application started");
  logger.warn("Low memory detected");
  logger.error("Database connection failed");
}

runApplication(new ConsoleLogger());
runApplication(new FileLogger("app.log"));

[UNIQUE INSIGHT]: The real performance difference between abstract class and interface in TypeScript is zero at runtime. Both compile to plain JavaScript. The choice is entirely about design intent and maintainability signals you send to other developers reading your code. Choosing abstract class says "these things share an ancestry." Choosing interface says "these things share a promise."


Decision Flowchart — Which One Should You Use?

START: I need to define shared structure for multiple classes
           |
           v
   Do the classes share actual STATE (fields/properties)?
           |
     YES --+-- NO
     |              |
     v              v
Use ABSTRACT     Do the classes share CONCRETE BEHAVIOR
CLASS            (implemented methods they all inherit)?
                          |
                    YES --+-- NO
                    |              |
                    v              v
              Use ABSTRACT     Will a class need to
              CLASS            commit to this structure
                               ALONGSIDE other unrelated
                               contracts?
                                         |
                                   YES --+-- NO
                                   |              |
                                   v              v
                              Use INTERFACE   Either works.
                                             Prefer INTERFACE
                                             for looser coupling.
           |
           v
   SPECIAL CASE: Do you need multiple inheritance?
   (class must inherit from more than one parent structure)
           |
     YES --+-- NO
     |              |
     v              v
Use INTERFACE(s)   Either can work,
only              check the above
                  questions first

-----------------------------------------------------------------------
QUICK RULES
-----------------------------------------------------------------------
Use ABSTRACT CLASS when:
  - Subclasses share fields (state)
  - Subclasses share some method implementations
  - You want to enforce a "kind of" relationship (is-a)
  - You need constructors and access modifiers (private/protected)

Use INTERFACE when:
  - You only need a behavioral contract, no implementation
  - A class needs to satisfy multiple unrelated contracts
  - You want maximum flexibility and loose coupling
  - The classes implementing it don't share an ancestry
-----------------------------------------------------------------------

Abstract Class vs Interface visual 2


Multiple Inheritance — Why Interfaces Solve What Abstract Classes Cannot

TypeScript (and most OOP languages) does not allow a class to extend more than one class. This is called the diamond problem, and it's covered in full in Lesson 4.4. But you can implement as many interfaces as you need.

// Multiple inheritance through interfaces
interface Flyable {
  fly(): void;
  getLandingSpeed(): number;
}

interface Swimmable {
  swim(): void;
  getDiveDepth(): number;
}

interface Runnable {
  run(): void;
  getTopSpeed(): number;
}

// A Duck can fly, swim, AND run
// This is impossible with abstract class multiple inheritance
class Duck implements Flyable, Swimmable, Runnable {
  fly(): void {
    console.log("Duck is flying at low altitude");
  }

  getLandingSpeed(): number {
    return 15; // km/h
  }

  swim(): void {
    console.log("Duck is paddling on the water");
  }

  getDiveDepth(): number {
    return 2; // meters
  }

  run(): void {
    console.log("Duck is waddling quickly");
  }

  getTopSpeed(): number {
    return 8; // km/h
  }
}

// A Submarine can only swim
class Submarine implements Swimmable {
  swim(): void {
    console.log("Submarine is navigating underwater");
  }

  getDiveDepth(): number {
    return 500; // meters
  }
}

const duck = new Duck();
duck.fly();  // Output: Duck is flying at low altitude
duck.swim(); // Output: Duck is paddling on the water
duck.run();  // Output: Duck is waddling quickly

// Polymorphism: treat duck as a Swimmable wherever one is needed
const swimmers: Swimmable[] = [new Duck(), new Submarine()];
swimmers.forEach(s => s.swim());
// Output:
// Duck is paddling on the water
// Submarine is navigating underwater

The Duck satisfies three independent contracts at once. If Flyable, Swimmable, and Runnable were abstract classes instead, this would be impossible without rewriting the entire inheritance chain.

[INTERNAL-LINK: diamond problem and multiple inheritance workarounds → Lesson 4.4: Multiple Inheritance and The Diamond Problem]


Common Mistakes

  • Using an abstract class when there is no shared state or implementation. If every method in your abstract class is abstract and there are no fields, you have written an interface using the wrong tool. Switch to an interface. It signals design intent accurately and removes unnecessary coupling.

  • Using an interface when the implementing classes genuinely share behavior. Forcing every subclass to duplicate the same implementation independently defeats the purpose of code reuse. If three classes all implement serialize() in exactly the same way, put that logic in an abstract class once.

  • Thinking "abstract class is more powerful, so I should default to it." More capability is not always better. Interfaces keep things loosely coupled. The SOLID principle of Dependency Inversion (Lesson 7.5) specifically recommends depending on interfaces over concrete classes.

  • Forgetting that TypeScript interfaces disappear at runtime. TypeScript compiles to JavaScript. Interfaces exist only during type-checking. You cannot do instanceof checks against an interface at runtime. Abstract classes compile to actual JavaScript constructor functions, so instanceof works on them.

  • Implementing an interface and leaving methods empty. An empty method body is not a valid implementation. If you write serialize(): string { return ""; } just to satisfy the interface, you have broken the contract's intent. This is called the "Liskov Substitution Principle" violation and is covered in Lesson 7.3.


Interview Questions

Q: What is the key difference between an abstract class and an interface? An abstract class can hold implemented methods, instance fields, constructors, and access modifiers. It enforces an "is-a" relationship and allows one level of inheritance. An interface is a pure contract with signatures only. It enforces a "can-do" relationship and allows a class to implement multiple interfaces simultaneously.

Q: Can you extend multiple abstract classes in TypeScript? No. TypeScript follows the single inheritance rule for classes. A class can extend only one class, abstract or concrete. This is a deliberate design decision in most class-based OOP languages to avoid the diamond problem. Interfaces solve the multiple inheritance need by allowing a class to implement as many contracts as required.

Q: When would you choose an abstract class over an interface? Choose an abstract class when the subclasses share actual state (instance fields) or share concrete method implementations. The abstract class lets you define the shared code once and inherit it. Choose an interface when you only need a behavioral contract with no shared implementation and when unrelated classes need to satisfy the same contract independently.

Q: Your abstract class has no concrete methods and no instance fields. Is that a valid design? It works syntactically, but it is poor design. An abstract class with no shared state and no concrete methods is functionally identical to an interface, with the added downside of consuming the single inheritance slot. The correct tool in that case is an interface. An abstract class earns its place only when it adds shared implementation or shared state on top of the forced-override contract.

Q: Can an interface extend another interface in TypeScript? Yes. Interfaces can extend one or more other interfaces. This lets you build up composite contracts. For example, interface ReadWriteStream extends Readable, Writable {} creates a combined contract. A class implementing ReadWriteStream must satisfy all methods from both Readable and Writable. Abstract classes can also extend other classes (abstract or concrete) and implement interfaces at the same time.


Quick Reference — Cheat Sheet

ABSTRACT CLASS vs INTERFACE — QUICK REFERENCE
-----------------------------------------------------------------------
                    ABSTRACT CLASS          INTERFACE
-----------------------------------------------------------------------
Has implementation  Yes (partial)           No (signatures only)
Has state (fields)  Yes                     No
Has constructor     Yes                     No
Access modifiers    Yes (public/            No (all members
                    protected/private)      are public)
Multiple inherit.   No (extend one only)    Yes (implement many)
Instantiable        No                      No
instanceof check    Yes (at runtime)        No (TypeScript only,
                                           erased at compile time)
Keyword             abstract class          interface
Usage keyword       extends                 implements
-----------------------------------------------------------------------

WHEN TO USE WHICH
-----------------------------------------------------------------------
Use ABSTRACT CLASS when:
  - Shared state (fields) across subclasses
  - Shared concrete method implementations
  - "Is-a" relationship (Dog is-an Animal)
  - Need constructors or access modifier control

Use INTERFACE when:
  - Pure behavioral contract, zero implementation
  - Class must satisfy multiple unrelated contracts
  - "Can-do" relationship (Duck can-fly, can-swim)
  - Maximum loose coupling needed
  - Depends on abstraction, not concrete ancestor

MULTIPLE INHERITANCE
-----------------------------------------------------------------------
  class MyClass extends AbstractBase          // 1 max
              implements InterfaceA,
                         InterfaceB,
                         InterfaceC {}        // unlimited

RUNTIME BEHAVIOR (TypeScript compiled to JavaScript)
-----------------------------------------------------------------------
  abstract class -> becomes a real JS class (constructor function)
  interface      -> erased completely, no JS output

INTERFACE EXTENDING INTERFACES
-----------------------------------------------------------------------
  interface C extends A, B {}   // valid — combines contracts
  class D implements C {}        // D must satisfy A + B + C
-----------------------------------------------------------------------

Abstract Class vs Interface visual 3


Previous: Lesson 3.3 - Interface Next: Lesson 3.5 - Abstraction in JavaScript


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

On this page