Abstract Class vs Interface

Beginner · 11 min read · ▶ live playground · ✦ checkpoint

Two guided tours share a name, duration, and summary calculation. They are also bookable, while only one supports an audio track. Put everything in one parent and unrelated bookable objects cannot join; put everything in interfaces and both tours repeat the same state and summary code.

An abstract class is a class you cannot instantiate directly that may provide shared state and a partial implementation for its children. An interface is a pure contract: it declares the operations a type must provide without storing instance state or supplying method bodies.

Both define a type callers can depend on. The choice is whether descendants need common machinery or only a common promise.

Put shared machinery in one place

A partial implementation supplies some finished behavior while deliberately leaving other behavior for a child. GuidedTour can store a tour's identity and build its summary, but each concrete tour must decide its own route.

class diagram

Tours inherit machinery and implement capabilities

Tours inherit machinery and implement capabilities
  1. 1The abstract GuidedTour stores shared state and implements summary(), but leaves route() unfinished.
  2. 2MuseumTour extends one abstract class and supplies the missing route().
  3. 3GardenTour receives the same state and summary machinery through inheritance.
  4. 4Reservable is an interface: MuseumTour promises reserve() without inheriting state.
  5. 5GardenTour can implement that same capability while keeping its own route behavior.
  6. 6MuseumTour implements a second interface; GardenTour does not claim audio support.

The two solid hollow-triangle arrows point to the one base class. The dashed hollow-triangle arrows point to capability contracts. MuseumTour extends one class and implements two interfaces, so the diagram separates shared construction from optional promises.

An abstract class can hold state and finished code

An abstract method is a method declaration without a body that every concrete child must implement. The abstract class can call that method inside finished behavior even though the child supplies the eventual code.

abstract class GuidedTour {
  private name: string;
  private durationMinutes: number;
 
  public constructor(name: string, durationMinutes: number) {
    this.name = name;
    this.durationMinutes = durationMinutes;
  }
 
  public summary(): string {
    return `${this.name} (${this.durationMinutes} min): ${this.route()}`;
  }
 
  public abstract route(): string;
}
 
interface Reservable {
  reserve(visitorName: string): string;
}
 
interface AudioSupported {
  audioTrack(): string;
}
 
class MuseumTour
  extends GuidedTour
  implements Reservable, AudioSupported
{
  public constructor() {
    super("Gallery highlights", 45);
  }
 
  public override route(): string {
    return "east wing -> sculpture hall";
  }
 
  public reserve(visitorName: string): string {
    return `${visitorName} reserved the museum tour`;
  }
 
  public audioTrack(): string {
    return "Audio track: gallery-highlights.mp3";
  }
}
 
class GardenTour extends GuidedTour implements Reservable {
  public constructor() {
    super("Rose garden walk", 30);
  }
 
  public override route(): string {
    return "south gate -> glasshouse";
  }
 
  public reserve(visitorName: string): string {
    return `${visitorName} reserved the garden tour`;
  }
}
 
const tours: GuidedTour[] = [
  new MuseumTour(),
  new GardenTour(),
];
 
for (const tour of tours) {
  console.log(tour.summary());
}
 
const museumTour = new MuseumTour();
console.log(museumTour.reserve("Mina"));
console.log(museumTour.audioTrack());

Gallery highlights (45 min): east wing -> sculpture hall Rose garden walk (30 min): south gate -> glasshouse Mina reserved the museum tour Audio track: gallery-highlights.mp3

GuidedTour owns the two fields, their constructor setup, and the finished summary() method. Its route remains abstract. That makes new GuidedTour(...) invalid: the class is incomplete until a concrete child supplies route().

The super(...) call runs the parent constructor so the inherited name and duration start in a valid state. Each child passes its values once and reuses the summary behavior.

Polymorphism still applies. The tours array uses the abstract base type, and summary() dynamically reaches the correct child route. An abstract class is incomplete, but it is still a useful type for a reference.

An interface keeps only the promise

Reservable does not say where reservations are stored, how capacity is checked, or which base class an implementation must use. It says only that callers may provide a visitor name and receive a result string.

TypeScript uses single inheritance, which means a class can extend only one parent class. A class can implement multiple interfaces. MuseumTour therefore receives shared machinery from GuidedTour while independently promising both Reservable and AudioSupported.

That makes interfaces a good fit for capabilities that can cross unrelated families. A guided tour, a workshop, and a ferry trip might all be reservable without sharing state, a constructor, or an honest is-a parent.

One class can use both. The abstract base answers “what construction and behavior do these tours share?” The interfaces answer “which capabilities may a caller rely on?” They solve different parts of the model.

Compare the trade-offs

Reach for an abstract class when all three statements are true:

  1. The types form a genuine is-a family.
  2. They share state, constructor rules, or useful method code.
  3. Spending the class's one inheritance relationship on that base is acceptable.

Reach for an interface when you need a small contract, expect unrelated types to provide it, or want one class to promise several capabilities. Interfaces also keep callers away from constructor and storage choices they do not need to know.

Neither option guarantees good abstraction. An abstract class with twenty unrelated methods is still a crowded parent. An interface with every method any implementation might someday need is still speculative. Keep the shared surface focused on what callers actually use.

When not to default to an abstract class

Do not create an abstract class only to force one method name. It consumes the class's single parent slot and couples children to a base they may not otherwise need. If reserve() is the whole shared idea, the small Reservable interface expresses it without imposing state or ancestry.

TypeScript also uses structural typing, which means compatibility depends on the members a value has, not only on an explicit implements declaration:

interface Labelled {
  label(): string;
}
 
function printLabel(item: Labelled): string {
  return item.label();
}
 
const lobbyKiosk = {
  label(): string {
    return "Lobby tour kiosk";
  },
};
 
console.log(printLabel(lobbyKiosk)); // Lobby tour kiosk

lobbyKiosk never writes implements Labelled, yet it has the required method, so TypeScript accepts it. An interface is still valuable as the name of the contract; explicit implements is not the only way a value can satisfy that contract.

Do not swing to the other extreme and ban abstract classes. If every tour repeats the same fields, validation, and summary algorithm, interfaces alone leave that implementation duplicated. Use the abstract base when shared machinery is real, stable, and belongs to a genuine family.

Checkpoint

Answer all three to mark this lesson complete

You now have the foundation's main tools: objects, boundaries, abstractions, type families, runtime variation, composed parts, and precise relationships. Next, SOLID—five principles for organizing responsibilities and dependencies—starts by asking how many reasons one class has to change.

+50 XP on completion