16.5 Abstract Classes and Methods
Right, so you’ve got classes. They’re blueprints, they’re cookie cutters, they’re a way to organize your code into neat little bundles of data and functionality. But sometimes, a blueprint is too specific. Sometimes you want to define the general shape of the thing—the fact that it must have doors and windows—but you don’t want to specify what kind of hinges the doors use. You want to leave that crucial, messy detail to the people actually building the house.
That’s the entire point of abstract classes. Think of them as the stern, visionary architect of your class hierarchy. They lay down the law: “Any building that inherits from my ‘Building’ plan must have a calculateOccupancy() method.” But they don’t deign to tell you how to calculate it for a skyscraper versus a bungalow. That’s beneath them.
An abstract class is a class that cannot be instantiated directly with new. It exists solely to be extended. It’s a foundation, not a finished product. The keywords abstract is how you tell TypeScript, “This class is a set of rules and partial ideas; don’t let anyone try to make one directly.”
The abstract Method: A Contract Without an Implementation
The most important tool in the abstract class’s toolbox is the abstract method. You declare it, but you don’t write its body. You’re saying, “Whoever extends this class, I don’t care how you do it, but you must provide an implementation for this method.”
abstract class Department {
constructor(public readonly name: string) {}
// Concrete method: has an implementation, inheritors get it for free.
describe(): void {
console.log(`Department: ${this.name}`);
}
// Abstract method: no implementation, just a signature.
// This is the contract. Inheriting classes MUST implement this.
abstract printMeeting(): void;
}
// Try to instantiate the abstract class? Nope.
// const bad = new Department("Accounting"); // Error: Cannot create an instance of an abstract class.
class AccountingDepartment extends Department {
constructor() {
super("Accounting and Financial Excellence");
}
// We MUST implement the abstract method, or TypeScript will throw a fit.
printMeeting(): void {
console.log("The accounting meeting starts at 10 AM sharp. Bring coffee.");
}
// We can also add our own stuff, no problem.
generateReports(): void {
console.log("Generating quarterly financial reports...");
}
}
const accounting = new AccountingDepartment();
accounting.describe(); // "Department: Accounting and Financial Excellence" <-- Inherited
accounting.printMeeting(); // "The accounting meeting starts at 10 AM sharp..." <-- Our implementation
accounting.generateReports(); // "Generating quarterly financial reports..." <-- Our new method
The beauty here is that the base Department class can define a method runMeeting() that calls this.printMeeting(). It doesn’t know how printMeeting() works, but it knows that by the time the code runs, a concrete implementation will exist. It’s enforcing a structure that allows for powerful polymorphism.
Why Not Just Use Interfaces?
Ah, the million-dollar question. Both interfaces and abstract classes define contracts. The difference is in the details (and it’s a difference the TC39 committee has been wrestling with for years for native JavaScript).
An interface is only a contract. It’s a list of requirements. It provides no implementation whatsoever. An abstract class, on the other hand, can provide some implementation. It’s a contract plus a partial blueprint.
Use an interface when you only care about the shape of the data and the names of the methods. Use an abstract class when you want to share actual code (methods or properties) between multiple related classes while still enforcing a specific contract.
// Pure contract: "You must have these things."
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}
// Contract + partial implementation: "You must have these things, and here's some code to get you started."
abstract class ClockAbstract {
constructor(public currentTime: Date) {}
// Concrete method
tick(): void {
console.log("tick tock");
}
// Abstract method
abstract setTime(d: Date): void;
}
The abstract class gives you a free tick() method. The interface gives you nothing but rules. Choose based on whether you need to share behavior or just structure.
Common Pitfalls and the “Oops” Moments
Forgetting the Implementation: This is the big one. You extend an abstract class, merrily code away, and then get a TypeScript error:
Non-abstract class 'MyClass' does not implement inherited abstract member 'myMethod' from class 'MyAbstractClass'. It’s a helpful error! It’s the compiler doing its job, reminding you of the contract you signed. Heed it.Trying to Instantiate the Abstract Class: This is a compile-time error, so it’s easy to catch. But it usually happens when you’re new to the concept and think, “Well, it’s a class, right?” Yes, but it’s an incomplete one. It’s a recipe, not a cake.
Overusing Them: Abstract classes are a powerful tool, but don’t reach for them for every hierarchy. If you can get away with a simple interface, do it. It’s more flexible. You can implement multiple interfaces, but you can only extend one class (abstract or not). Reserve abstract classes for when you genuinely have shared implementation code to offer your children.
The designers made a solid choice here, honestly. It fits perfectly into the class-based OOP model TypeScript emulates, providing a crucial middle ground between a fully implemented class and a bare-bones interface. It’s the “here’s the plan and some starter tools, now you finish the job” pattern, and when used appropriately, it’s brilliantly effective.