When developing robust and scalable software applications, introducing abstract classes in TypeScript is often a key architectural decision. An abstract class is the right solution when we want to define a shared foundation for a group of related objects, providing common, already implemented state and behavior (the DRY principle). In other words, an abstract class defines what its subclasses share, and what they are required to implement in order to be concrete and functional.
Design Balance: Implemented Methods and Abstract Requirements
The strength of an abstract class lies in its ability to balance shared implementation with required customization. This division is expressed through two components that coexist within an abstract structure:
**Shared Behavior (Concrete M…
When developing robust and scalable software applications, introducing abstract classes in TypeScript is often a key architectural decision. An abstract class is the right solution when we want to define a shared foundation for a group of related objects, providing common, already implemented state and behavior (the DRY principle). In other words, an abstract class defines what its subclasses share, and what they are required to implement in order to be concrete and functional.
Design Balance: Implemented Methods and Abstract Requirements
The strength of an abstract class lies in its ability to balance shared implementation with required customization. This division is expressed through two components that coexist within an abstract structure:
Shared Behavior (Concrete Methods and Properties) This part of the abstract class includes all methods and properties that are fully functional and implemented. Subclasses automatically inherit this implementation without needing to rewrite it. 1.
Abstract Imperative (Required Methods)
These are methods that define a signature but lack an implementation. By declaring a method as abstract, we are not offering an option, we are enforcing that every subclass must provide its own implementation.
abstract class ApiClient<T> {
// Shared behavior
async getAll(): Promise<T[]> {
const res = await fetch(this.getEndpoint());
return res.json();
}
// Required behavior: each subclass MUST return its own endpoint
protected abstract getEndpoint(): string;
}
interface User { id: number; name: string; }
interface Product { id: number; title: string; }
// Concrete class for users
class UserClient extends ApiClient<User> {
protected getEndpoint(): string {
return "/api/users";
}
}
// Concrete class for products
class ProductClient extends ApiClient<Product> {
protected getEndpoint(): string {
return "/api/products";
}
}
// Usage
const users = new UserClient();
users.getAll()
const products = new ProductClient();
products.getAll()
Design Control and Discipline
The greatest contribution of an abstract class is its ability to enforce structural discipline and protect the codebase from inconsistencies. Abstract classes ensure that:
Key functionality is never forgotten: Declaring an essential method as abstract guarantees that no subclass can be created without providing a concrete implementation of that method.
Consistent Behavior (Polymorphism): Different subclasses can provide different implementations, but from the outside they can all be treated uniformly through the shared abstract type.
Abstraction serves as the first line of defense in complex systems, ensuring that the core structure and identity of an object aren’t bypassed or ignored.
Using abstract classes is a way to ensure a clear architectural structure: shared logic is centralized, and essential behavior cannot be omitted. The result is more organized, readable, and stable code.