Open-Closed Principle (OCP) Best Practices in TypeScript: Guidelines and Examples
Every time we added a new payment method, we modified the PaymentProcessor class. Add Stripe? Touch PaymentProcessor. Add PayPal? Touch PaymentProcessor. ...
19 Apr 2024

Every time we added a new payment method, we modified the PaymentProcessor class. Add Stripe? Touch PaymentProcessor. Add PayPal? Touch PaymentProcessor. Add crypto? Touch PaymentProcessor. Each change risked breaking the existing methods. Eventually, someone did break credit card processing while adding Apple Pay.
The Open-Closed Principle exists to prevent exactly this.
What OCP says
Software entities — classes, functions, modules — should be open for extension but closed for modification. You should be able to add new behavior without changing existing code.
This doesn't mean code is frozen forever. It means you design it so that new capabilities plug in rather than requiring surgery on the existing implementation.
How to apply OCP in TypeScript
Use abstractions and interfaces
Define contracts. Let different implementations fulfill them. New behavior means a new class, not a modified one.
interface Shape {
area(): number;
}
class Circle implements Shape {
constructor(private radius: number) {}
area(): number {
return Math.PI * this.radius ** 2;
}
}
class Rectangle implements Shape {
constructor(private width: number, private height: number) {}
area(): number {
return this.width * this.height;
}
}
Adding a Triangle means creating a new class that implements Shape. The existing Circle and Rectangle classes don't change. Nothing breaks.
Use dependency injection
Inject dependencies through constructors. The class depends on the abstraction, not the concrete implementation.
class Logger {
log(message: string): void {
console.log(message);
}
}
class ProductService {
constructor(private logger: Logger) {}
saveProduct(product: Product): void {
this.logger.log(`Product saved: ${product.name}`);
}
}
Want a different logging strategy? Inject a different logger. ProductService stays closed.
Use the Strategy pattern
Encapsulate algorithms behind an interface. Swap strategies at runtime without modifying the code that uses them.
interface SortingStrategy {
sort(items: any[]): any[];
}
class QuickSort implements SortingStrategy {
sort(items: any[]): any[] {
// quicksort implementation
return items;
}
}
class MergeSort implements SortingStrategy {
sort(items: any[]): any[] {
// merge sort implementation
return items;
}
}
class Sorter {
constructor(private strategy: SortingStrategy) {}
sort(items: any[]): any[] {
return this.strategy.sort(items);
}
}
New sorting algorithm? Create a new class. The Sorter never changes. See also: Strategy Design Pattern
Design extension points
Build hooks, events, or plugin systems that let external code add behavior without modifying the core.
class Button {
private handlers: (() => void)[] = [];
onClick(callback: () => void): void {
this.handlers.push(callback);
}
click(): void {
this.handlers.forEach(handler => handler());
}
}
const button = new Button();
button.onClick(() => console.log('Clicked!'));
button.onClick(() => analytics.track('button_click'));
The Button class is closed for modification. But it's open for extension — you can add any number of click handlers without touching the class.
The trade-off
OCP requires upfront design. You need to identify the axis of change — what's likely to vary — and build the extension point before you need it. Get it wrong and you've added abstraction for nothing. Get it right and adding features becomes trivially easy.
The risk is over-engineering. Not every class needs to be extensible. Apply OCP where you see a pattern of repeated modification. If a class changes every sprint, that's a signal it needs extension points. If it hasn't changed in a year, leave it alone.
Keep reading
- DI, SOLID, and the Fundamentals That Actually Matter
- Coding Principles for Better Code Quality
- Right Balance: Best Practices for Code Comments in TypeScript
- Best Practices for Writing Isolated Tests in TypeScript
- Meaningful Test Concepts: Best Practices for Writing Tests in TypeScript
- Secure Programming and Resilience (SPR) Best Practices in TypeScript