Articles

TypeScript Abstract Classes, Explained

Abstract classes in TypeScript define shared implementation plus methods subclasses must fill in. How they differ from interfaces and when to reach for them.

Takina Takina · · 4 min read
Code editor showing TypeScript class definitions

An abstract class in TypeScript is a class that can’t be instantiated directly — it exists to be extended, bundling shared implementation with one or more methods that subclasses are required to fill in. It sits between a plain class (fully implemented, ready to use) and an interface (pure shape, no implementation at all).

Basic syntax

Mark a class abstract, and optionally mark individual methods abstract too — those get a signature but no body:

abstract class Shape {
  abstract area(): number;

  describe(): string {
    return `This shape has an area of ${this.area()}`;
  }
}

class Circle extends Shape {
  constructor(private radius: number) {
    super();
  }

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

new Shape();        // Error: Cannot create an instance of an abstract class
new Circle(4).describe(); // "This shape has an area of 50.26..."

Shape can’t be instantiated on its own — TypeScript rejects new Shape() at compile time. Circle, which provides a concrete area(), can be instantiated. describe() is fully implemented on the base class and inherited as-is; only area() is left for each subclass to define.

Abstract classes vs. interfaces

Both describe a contract that implementing types must satisfy, but they differ in what they’re allowed to carry.

Abstract classInterface
ImplementationCan include concrete methods and fieldsNone — pure shape
InstantiationCannot be instantiated directlyN/A — not a runtime construct at all
InheritanceSingle inheritance (extends)A class can implement many interfaces
ConstructorsCan have one, called via super()No constructors
Compiles toReal JavaScript class (abstract keyword is erased, structure remains)Erased entirely — no runtime trace
Access modifiersSupports private/protected membersAll members implicitly public

The practical rule of thumb: reach for an interface when you’re describing a shape that unrelated classes might satisfy independently — no shared behavior, just a contract. Reach for an abstract class when several related classes share real, non-trivial implementation and you want to write that logic once rather than duplicating it across every subclass.

Why not just use a regular base class?

You technically can — nothing stops you from writing a normal class with a method that throws "not implemented" and expecting subclasses to override it. The difference is when the mistake gets caught. With an unenforced base class, forgetting to override a method is a runtime error, discovered only when that code path executes. With an abstract method, the compiler refuses to build until every concrete subclass provides an implementation — the same category of shift-left benefit you get from TypeScript’s type guards narrowing checks from runtime to compile time.

Marking the class itself abstract closes a second gap: it stops anyone from instantiating the base type directly and calling an abstract method that has no body, which would otherwise throw immediately at runtime.

A more realistic example

Abstract classes tend to earn their keep in scenarios with shared orchestration logic and a few points of required customization — parsers, connectors, and processing pipelines are common cases:

abstract class DataSource<T> {
  async fetchAll(): Promise<T[]> {
    const raw = await this.fetchRaw();
    return raw.map((item) => this.parse(item));
  }

  protected abstract fetchRaw(): Promise<unknown[]>;
  protected abstract parse(item: unknown): T;
}

class UsersSource extends DataSource<{ id: string; name: string }> {
  protected async fetchRaw() {
    const res = await fetch("/api/users");
    return res.json();
  }

  protected parse(item: unknown) {
    const u = item as { id: string; name: string };
    return { id: u.id, name: u.name };
  }
}

fetchAll() — the orchestration — is written once, on the base class. Each concrete source only has to supply fetchRaw() and parse(). Note the protected modifiers: those two methods are implementation details of the fetch pipeline, not something external callers should invoke directly. That combination of shared flow control plus enforced, encapsulated customization points is difficult to express cleanly with an interface alone, since an interface can’t provide fetchAll()’s body.

Where they get in the way

Abstract classes come with the same trade-offs as class inheritance generally: a subclass can only extend one abstract class, so if a type needs to satisfy two independent contracts, at most one of them can be an abstract class — the rest have to be interfaces. Deep abstract class hierarchies also tend to accumulate the classic inheritance problem of behavior that’s hard to trace, spread across several ancestor classes. If composition — building behavior out of small, combinable pieces — solves the problem as well as inheritance does, it’s usually the easier code to maintain later.

The takeaway

An abstract class lets you write shared implementation once while forcing subclasses to fill in the pieces that must vary, with the compiler enforcing it rather than a runtime check. Use one when related classes share real behavior and you want required customization points caught at build time; reach for a plain interface when you’re only describing a shape, and prefer composition over deep inheritance chains once a hierarchy starts getting hard to follow.

Takina Takina · · 3 min read

TypeScript's never Type, Explained

never represents values that can't exist — it marks unreachable code, exhaustive switches, and functions that always throw or loop forever.

#TypeScript #JavaScript #Web Development
Takina Takina · · 4 min read

TypeScript readonly Modifiers Explained

TypeScript's readonly keyword blocks reassignment at compile time for properties, arrays, and tuples — with no runtime enforcement at all.

#TypeScript #JavaScript #Web Development
Takina Takina · · 4 min read

TypeScript's `as const`, Explained

TypeScript's as const assertion locks a value to its literal, readonly type instead of widening it. How it works and when to reach for it.

#TypeScript #JavaScript #Web Development