Ch. 2 · TypeScript

TypeScript Abstract Classes and Contracts

Share behavior with abstract classes, enforce required members, and decide when an interface or composition is the better contract.

~2 min readadvancedupdated Oct 5, 2026

An abstract class cannot be instantiated directly and may mix abstract members, which subclasses must implement, with concrete methods that carry shared behavior. It is the right tool when subclasses must share code and invariants; an interface is better when you only need to describe a shape.

Before you start

You should be comfortable with classes, inheritance and interfaces. This article contrasts abstract classes with interfaces and composition; it assumes familiarity with implements.

Step-by-step walkthrough

Step 1: Declare the members subclasses must supply

An abstract method has no body and must be implemented by every concrete subclass. This lets the base class call this.run() in a template method while leaving the variation to subclasses, and the compiler guarantees that a concrete subclass cannot forget it.

Step 2: Put shared behavior and invariants in the base

The base class can hold fields, validation and the surrounding algorithm. Because it can define protected helpers, it can enforce an invariant once instead of in every subclass, which is the main advantage over an interface that shares no code.

Step 3: Weigh inheritance against composition

Abstract classes require inheritance, so they carry a single-base constraint and break structural typing: a structurally identical object cannot satisfy an abstract class type. When the contract is pure shape with no shared code, prefer an interface; when behavior varies but the algorithm is shared, an abstract class is justified.

Worked scenario

The base class owns the timing, and the subclass supplies the work.

abstract class Job {
  abstract run(): Promise<void>;
  async execute(): Promise<void> {
    const start = Date.now();
    await this.run();
    console.log(`took ${Date.now() - start}ms`);
  }
}
class EmailJob extends Job {
  async run(): Promise<void> {}
}
await new EmailJob().execute();
TypeScript

Walk through the example

execute is the template method: it measures time and delegates the actual work to the abstract run. EmailJob must implement run or it stays abstract and cannot be instantiated, so the compiler enforces the contract. A caller that only cares about running a job depends on Job, not on EmailJob.

Common mistake

Using an abstract class for a pure interface, which forces consumers into inheritance and blocks structural substitutes such as a test double. A related mistake is putting too much logic in the base so subclasses cannot vary it, which turns the template method into a fragile hierarchy.

Verify the behavior

Try instantiating the abstract class and confirm the compiler rejects it. Create a subclass that omits the abstract method and confirm the error. Write a function that accepts the abstract type and pass a subclass, then confirm that an object literal with the same shape is not accepted, demonstrating the lack of structural compatibility.

Interview exercise

You have three payment providers with a shared retry-and-log wrapper. Abstract class or interface?

Answer and reasoning

An abstract class fits because the retry and logging behavior is shared code that belongs in one place, and each provider only supplies the provider-specific call. If the wrapper were stateless and each provider already had its own class, an interface plus a standalone helper function would compose better and avoid inheritance.

Continue learning

Compare contracts in type versus interface and structural typing. Read the TypeScript classes handbook and try the TypeScript MCQs.

More in TypeScript

read ✓TypeScript · mid

TypeScript Enums vs Union Types

Compare enums with unions of string literals: runtime cost, serialization, exhaustiveness and which one fits application code.

~3 min readread →
esc