Abstract Classes
What happens when a class is too general to exist on its own? Master Abstract Classes to build the perfect foundation for your hierarchies.
Sometimes, an "Idea" is so general that it shouldn't have a physical body. For example, you can have a "Dog" or a "Cat," but have you ever seen a generic "Animal" walking down the street? No.
To represent these general ideas that shouldn't be instantiated, we use Abstract Classes.
The Essentials
The "Incomplete Blueprint" guide:
- No Instantiation: You cannot use the
newkeyword on an abstract class. - Partial Implementation: Abstract classes can have both "Concrete" methods (with code) and "Abstract" methods (without code).
- The Base Layer: They serve as the perfect parent for a family of related classes.
- Mandatory Overriding: Any child class must implement all the abstract methods of its parent.
The Half-Built House Analogy
Imagine a construction company that sells "Standard Home Foundations."
- The Concrete Part: Every foundation comes with plumbing and electrical wiring pre-installed. (Concrete Methods)
- The Abstract Part: The company doesn't decide what the kitchen looks like. They leave an "Empty Space" labeled "Kitchen" and tell you: "You must build a kitchen here." (Abstract Methods)
An Abstract Class is that foundation. It gives you the basics for free but forces you to finish the specific details.
Abstract Classes in Action: The Design Foundation
Can be instantiated directly. If it's too general (like "Animal"), you might end up with meaningless objects in memory.
makeSound() { .. }
}
let a = new Animal();
// What sound does a generic animal make?
Hard to enforce specific behaviors on children without complex overriding.
Cannot be instantiated. It provides shared "Concrete" logic while forcing children to implement "Abstract" details.
eat() { /* shared */ }
abstract sound();
}
class Dog extends Animal { .. }
Guarantees that every "Specific" animal has a sound, while sharing the eating logic.
Code Implementation: The Partial Blueprint
Here is how you combine shared state with mandatory abstract behavior:
abstract class Database {
// Concrete attribute: All DBs need a connection string
constructor(protected connectionString: string) {}
// Concrete method: Sharing the connection logic
connect() {
console.log(`Connecting to ${this.connectionString}...`);
}
// Abstract method: Every DB queries differently
abstract query(sql: string): void;
}
class SQLDatabase extends Database {
query(sql: string) {
console.log(`Running SQL: ${sql} on MySQL`);
}
}
// const db = new Database("..."); // Error! Cannot instantiate
const mysql = new SQLDatabase("localhost:3306");
mysql.connect();
mysql.query("SELECT * FROM users");The Diamond Problem: Why Multiple Inheritance is Forbidden
A common question is: "Why can a class implement multiple interfaces, but only extend ONE class?"
Imagine this scenario:
- Class A: Has a method
doTask(). - Class B: Extends A and overrides
doTask()to print "Task B". - Class C: Extends A and overrides
doTask()to print "Task C". - Class D: Tries to extend both B and C.
If you call d.doTask(), which one should run? B or C? This ambiguity is known as the Diamond Problem.
To prevent this "Identity Crisis," languages like Java and TypeScript forbid multiple inheritance for classes. Since interfaces only provide the what and not the how, they don't suffer from this problem: if two interfaces have the same method, the child just provides one implementation that satisfies both.
By using abstract classes, you ensure that your code is DRY (Don't Repeat Yourself) while still enforcing a strict structure on all your child classes.
We're almost done with the core pillars. Let's look at some special modifiers: Static & Final.
Practice what you just read.