Intermediate12 min readObject-Oriented Foundationslive prototype

Abstract Classes vs Interfaces

Both define contracts, but an abstract class is a shared partial base (state + some code, single inheritance) while an interface is a pure capability contract (no state, multiple allowed).

The idea

What it is

Abstract classes and interfaces both let you declare a contract — a set of methods that subtypes must provide — without fully implementing them up front. Because they overlap so much, beginners often treat them as interchangeable. They aren't. The difference is about what each one is allowed to carry and how many a class can take on.

Picture a job description. An abstract class is like a partly-trained employee: they already share company knowledge and some habits (real code and stored state), and a new hire becomes one of them — you can only have one such base. An interface is like a certification — "can drive", "can swim" — a promise of a capability with no baggage attached, and you can hold many certifications at once across totally unrelated people.

The one sentence to remember

Use an abstract class when subtypes are a kind of something and should share code and state. Use an interface when unrelated types just need to promise the same capability. Is-a → abstract class; can-do → interface.

Mechanics

How it works

What an abstract class can carry

An abstract class sits halfway between a normal class and a pure contract. It cannot be instantiated on its own, but it can hold almost everything a regular class can:

  • Fields / instance state — it can store data, like a name or a cached value, that every subclass inherits.
  • A constructor — it can run setup code when a subclass object is built (subclasses call super(...)).
  • Concrete methods — fully written methods that all subclasses get for free, plus abstract methods that subclasses are forced to fill in.

The catch: in most languages a class can extend exactly one abstract class. You inherit a single base, so it's the right home for a shared identity plus shared machinery.

What an interface can carry

An interface is a leaner thing — a contract of method signatures with (classically) no bodies and no stored state. A class implements an interface by providing real code for every method it names. The superpower is multiplicity: a single class can implement many interfaces at once, mixing capabilities from completely unrelated contracts.

contracts.ts
// Interface: just the capability, no state, no constructor.
interface Drawable {
  draw(): void; // a signature — the WHAT, no HOW
}

// One class can promise MANY unrelated capabilities at once:
class Widget implements Drawable, Serializable, Clickable {
  draw() { /* ... */ }
  serialize() { /* ... */ }
  onClick() { /* ... */ }
}

Modern languages blurred the line a little: since Java 8, interfaces can carry default methods (a shared body) and static methods — yet they still hold no instance state. TypeScript interfaces describe object shapes and are purely structural (they vanish at runtime). Python uses abc.ABC for abstract base classes and Protocol for interface-style structural typing. C++ has no separate interface keyword at all — an interface there is just an abstract class whose methods are all pure virtual (= 0).

The decision rule

When both seem to fit, ask two questions. Is there shared code or state every subtype should inherit, and is this an "is-a" relationship? → reach for an abstract class. Do unrelated types just need to advertise the same ability, with each writing its own version? → reach for an interface. And when you need both — shared state and the freedom to mix several capabilities — the idiomatic answer is to combine them: an abstract base for the shared part, interfaces for each extra capability.

Why "single base, many contracts" matters

Single inheritance keeps the shared-state story simple — there's exactly one base to reason about, so no ambiguity over whose name field you inherited. Interfaces sidestep that problem by carrying no state at all, which is why a language can safely let you implement as many as you like.

Interactive prototype

See it. Build it. Break it.

A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.

About this simulation

Two columns — Abstract Class and Interface — laid out side by side, each row showing what it can and can't do. Flip the requirement toggles on top to describe your situation, and watch the recommended column light up with a → use this badge while the log explains why. Ask for shared state and mixing several contracts at once, and it tells you to combine both.

Hands-on

Try these yourself

Open the prototype above, predict what happens, then verify.

try 01

Read the comparison grid

Before touching anything, scan the two columns. Notice where the badges differ: holds state and constructor are for the abstract class but for the interface, while how many reads one vs many. The row that's identical — forces a contract — is for both, which is exactly why they get confused.

try 02

Toggle a requirement and watch the pick

Flip I need to share common code on. The Abstract Class column lights up with a → use this badge and the log explains why — concrete methods live on a base, not on a pure interface. Now turn it off and flip I just need a capability contract; the recommendation jumps to the Interface column live.

try 03

Force the conflict

Turn on I need stored fields/state and one class must mix several of these at the same time. No single tool wins — so the log advises the combined pattern: an abstract base for the shared state plus interfaces for the extra capabilities. That's the real-world answer when requirements pull both ways.

In practice

When to use it — and what trips people up

Picking between the two

Default to an interface — it's the looser, more composable choice, and types that share nothing but a capability can still implement it. Promote to an abstract class the moment you find yourself wanting to share real code or stored state across subtypes that genuinely form an "is-a" family. When both pulls are real at once, don't choose — use an abstract base for the shared part and layer interfaces on top for the mix-in capabilities.

Don't force an is-a where there's only a can-do

Reaching for an abstract base just to share one helper method drags every subtype into a single inheritance line they may not belong in. If the types aren't truly the same kind of thing, an interface (or composition) keeps them free.

What it gives you

  • Reach for an abstract class when subtypes share an is-a relationship — a Circle and a Square are both genuinely a kind of Shape.
  • Reach for an abstract class when you have concrete code or stored state every subtype should inherit, like a shared name field and a describe() method.
  • Reach for an abstract class when you want a constructor to enforce valid setup for the whole family of subtypes.
  • Reach for an abstract class when single inheritance is fine and you want one clear, central base to evolve over time.

Common mistakes

  • Reach for an interface when unrelated types need the same ability — a FileButton and a Player can both be Clickable without sharing a family.
  • Reach for an interface when one class must mix several capabilities at once, since a class can implement many but extend only one.
  • Reach for an interface when you want a pure contract with no state — just signatures callers can depend on, each type writing its own version.
  • Reach for an interface when you're designing for maximum flexibility and testing, letting any type opt in to a capability without inheritance baggage.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

// Abstract class: shared state + a concrete method + an abstract one.
abstract class Shape {
  constructor(protected name: string) {} // stored state + constructor

  describe(): string {                    // concrete — shared by all shapes
    return `${this.name} with area ${this.area().toFixed(2)}`;
  }

  abstract area(): number;                // the WHAT — each shape fills in
}

// Interface: a pure capability contract, no state.
interface Drawable {
  draw(): void;
}

// Circle IS-A Shape (extends one base) and CAN-DO Drawable (implements).
class Circle extends Shape implements Drawable {
  constructor(private r: number) {
    super("circle");
  }
  area(): number {
    return Math.PI * this.r * this.r;
  }
  draw(): void {
    console.log(`drawing ${this.describe()}`);
  }
}

const c = new Circle(2);
console.log(c.describe()); // circle with area 12.57
c.draw();

References & further reading

4 sources

Knowledge check

Did it land?

Quick questions, answers revealed on submit. Nothing is scored or saved.

question 01 / 04

Which capability belongs to an abstract class but NOT to a classic interface?

question 02 / 04

A Widget needs to be drawable, serializable, and clickable — three unrelated capabilities. What's the natural fit?

question 03 / 04

Since Java 8, interfaces can include default methods with real bodies. Does that make them the same as abstract classes?

question 04 / 04

You need subtypes to share a stored name field AND mix in several unrelated capabilities. What's the idiomatic design?

0/4 answered