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();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.