The Open-Closed Principle (OCP) says software should be open for extension but closed for modification. Applied in TypeScript and React, OCP helps teams add features without touching battle-tested code, reducing regressions and making systems easier to evolve.
December 7, 2025 (3mo ago) — last updated January 26, 2026 (1mo ago)
Open-Closed Principle Guide (TypeScript & React)
Learn the Open-Closed Principle with TypeScript and React examples. Write scalable, maintainable code using interfaces, composition, and strategy patterns.
← Back to blog
Open-Closed Principle Guide (TypeScript & React)
Summary: Master the Open-Closed Principle with TypeScript and React examples. Write scalable, maintainable code using interfaces, composition, and the Strategy pattern.
Introduction
The Open-Closed Principle (OCP) says software should be open for extension but closed for modification. Applied correctly in TypeScript and React, OCP helps teams add features without touching battle-tested code, reducing regressions and making systems easier to evolve.
The Open-Closed Principle in plain terms
The Open-Closed Principle (OCP) sounds academic but is deeply practical: design modules so you can add behavior by adding new code, not by changing existing code. That reduces risk when shipping features and keeps core systems stable.
The foundation of adaptable code

Think of your software like a house with tested electrical wiring. You don’t tear down walls to add a TV; you plug into existing outlets. The wiring is closed for modification but open for extension. The same idea applies to code: protect proven implementation, expose stable contracts to extend behavior.
What “open for extension” means
Design modules, classes, or functions so their behavior can be augmented without changing their internals. Add capabilities by creating new modules that implement a stable contract.
What “closed for modification” means
Avoid editing stable, tested code just to add features. Each modification risks regressions; OCP minimizes those edits and localizes change.
OCP is a core SOLID guideline and works closely with Single Responsibility Principle and Dependency Inversion. When teams adopt OCP, they reduce maintenance churn and keep core systems stable while adding features rapidly1.
“Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification.”
How to implement OCP with TypeScript
Abstraction and polymorphism are your main tools. Use interfaces and abstract classes so components depend on contracts, not concrete implementations. When a module depends on an interface, you can add behavior by implementing new classes that satisfy that interface; the original module doesn’t change.

A common OCP violation
A frequent anti-pattern is a switch or long if/else that dispatches behavior by type. For example, a PaymentProcessor that switches on paymentMethod must be modified for every new method.
// BEFORE: Violates the Open-Closed Principle
type PaymentMethod = 'credit_card' | 'paypal' | 'crypto';
interface Order {
id: string;
amount: number;
paymentMethod: PaymentMethod;
}
class PaymentProcessor {
process(order: Order): void {
switch (order.paymentMethod) {
case 'credit_card':
console.log(`Processing credit card payment for order ${order.id}...`);
break;
case 'paypal':
console.log(`Processing PayPal payment for order ${order.id}...`);
break;
case 'crypto':
console.log(`Processing crypto payment for order ${order.id}...`);
break;
default:
throw new Error('Unsupported payment method');
}
}
}
Every new payment method forces edits to this class. It’s fragile and not closed for modification.
Refactoring to adhere to OCP
Introduce a PaymentStrategy interface and one class per payment method. The processor depends on the abstraction; adding a method means adding a new class, not changing existing code.
// AFTER: Adheres to the Open-Closed Principle
interface PaymentStrategy {
pay(order: Order): void;
}
class CreditCardPayment implements PaymentStrategy {
pay(order: Order): void {
console.log(`Processing credit card payment for order ${order.id}...`);
}
}
class PayPalPayment implements PaymentStrategy {
pay(order: Order): void {
console.log(`Processing PayPal payment for order ${order.id}...`);
}
}
class CryptoPayment implements PaymentStrategy {
pay(order: Order): void {
console.log(`Processing crypto payment for order ${order.id}...`);
}
}
class OcpPaymentProcessor {
process(order: Order, paymentStrategy: PaymentStrategy): void {
paymentStrategy.pay(order);
}
}
To add Apple Pay, create an ApplePayPayment class implementing PaymentStrategy. No existing code changes.
Applying OCP in React and Node.js
OCP is effective on both frontend and backend. Use composition in React and the Strategy pattern or provider interfaces in Node.js.

A flexible Card component in React
A Card component with a type prop and conditional rendering becomes a change magnet. Favor composition: make Card a stable wrapper and build specialized components that use it.
// BEFORE: Violates the Open-Closed Principle
interface CardProps {
type: 'user' | 'product' | 'news';
data: any;
}
const Card = ({ type, data }: CardProps) => {
if (type === 'user') {
return <div><h2>{data.name}</h2><p>{data.email}</p></div>;
} else if (type === 'product') {
return <div><h3>{data.title}</h3><span>${data.price}</span></div>;
} else if (type === 'news') {
return <div><h1>{data.headline}</h1><p>{data.summary}</p></div>;
}
return null;
};
// AFTER: Adheres to the Open-Closed Principle
interface CardProps {
children: React.ReactNode;
className?: string;
}
const Card = ({ children, className }: CardProps) => (
<div className={`card ${className}`}>{children}</div>
);
const UserProfileCard = ({ user }) => (
<Card className="user-profile">
<h2>{user.name}</h2>
<p>{user.email}</p>
</Card>
);
const ProductSummaryCard = ({ product }) => (
<Card className="product-summary">
<h3>{product.title}</h3>
<span>${product.price}</span>
</Card>
);
Now the base Card is closed for modification while the system remains open to new card types.
An extensible payment service in Node.js
Avoid switches on provider; use a PaymentProvider interface and concrete providers.
// AFTER: Adheres to the Open-Closed Principle
interface PaymentProvider {
process(amount: number): void;
}
class StripeProvider implements PaymentProvider {
process(amount: number) {
console.log(`Processing $${amount} via Stripe.`);
}
}
class PayPalProvider implements PaymentProvider {
process(amount: number) {
console.log(`Processing $${amount} via PayPal.`);
}
}
class PaymentService {
processPayment(provider: PaymentProvider, amount: number) {
provider.process(amount);
}
}
Adding Adyen or Braintree means adding a new provider class; the PaymentService remains untouched.
OCP violation vs. adherence patterns
| Scenario | Code Violating OCP (Before) | Code Adhering to OCP (After) |
|---|---|---|
| Rendering UI variants | Single component with a large switch on type | Generic container accepts specialized components as children |
| Data export logic | exportData(format, data) with if/else blocks | Exporter interface with CsvExporter, JsonExporter classes |
| Discount calculation | calculateDiscount with many if chains | DiscountStrategy interface with concrete strategies |
| Handling user actions | Massive reducer with one switch on action.type | Smaller reducers, composition, middleware |
| Backend routing | One routing file with dozens of routes | Modular routing with per-resource routers |
The “Before” column shows code that must change often; the “After” column shows patterns that favor adding isolated modules. That shift is the heart of OCP.
Common OCP mistakes and how to avoid them

The endless if/else or switch chain
This classic violation centralizes logic and forces modification whenever a new type is added. Move behavior into polymorphic classes that implement a shared interface.
// ANTI-PATTERN
function getAnimalSound(animal: { type: string }): string {
if (animal.type === 'dog') return 'Woof';
if (animal.type === 'cat') return 'Meow';
if (animal.type === 'cow') return 'Moo';
return '';
}
// REFACTORED
interface Animal { makeSound(): string }
class Dog implements Animal { makeSound() { return 'Woof' } }
class Cat implements Animal { makeSound() { return 'Meow' } }
Add a Sheep class to extend behavior without touching the existing code.
The fragile base class problem
When a base class provides too much concrete behavior, changes to it break subclasses. Prefer composition over inheritance and keep base classes minimal—define contracts, not heavy implementations.
The pitfall of premature generalization
Over-abstraction “just in case” leads to complexity. Apply OCP where change is likely: business rules, export formats, and third-party integrations. When code is modified repeatedly for the same reason, that’s the signal to refactor toward OCP.
By avoiding these mistakes, you’ll build systems ready for future growth without unnecessary complexity. Industry research highlights that design and modular practices help teams adapt to changing requirements and tooling environments2, and public-sector guidance often recommends modular patterns for maintainability3.
Why OCP matters for your business and team
OCP turns code from a liability into a flexible asset. When software is open for extension but closed for modification, you reduce the complexity tax that slows feature delivery.
Driving down maintenance costs
Maintenance often dominates software lifecycle costs; protecting stable code reduces development time, QA cycles, and costly bug fixes1.
Accelerating your team’s velocity
OCP requires thought up front, but with a stable foundation, teams add features faster and more predictably. Developers build on a platform, not inside a fragile core.
Mitigating risk
Every change carries risk. Adding a new, self-contained class is safer than editing battle-tested code. OCP creates boundaries that shield critical logic from accidental side effects.
Weaving OCP into your team’s workflow
Adoption is a team sport. Embed OCP into code reviews, training, and tooling.
Make OCP part of code reviews
Use PR checklists with questions like:
- “If we needed a new [type/strategy/option], would this file need to change?”
- “Is this
switchor longif/elselikely to grow?” - “Could this logic be pulled into a strategy or plugin?”
Focus reviews on business rules, integrations, and UI libraries that change most often.
Tools and training
Linters and static analysis can flag OCP anti-patterns. Hands-on workshops help teams internalize these design habits. For more on how these patterns fit with testing, see the Red-Green-Refactor TDD guide referenced below.
Common questions about OCP
“Does ‘closed for modification’ mean I can never touch a file again?”
No. It’s a guiding principle for adding features. Bug fixes and necessary maintenance are different. OCP is about avoiding edits to stable code when introducing new behavior.
“Isn’t this just over-engineering?”
It can be if applied everywhere. Be pragmatic: apply OCP where change is likely. When you modify the same code repeatedly, refactor toward OCP.
“How does OCP fit with the other SOLID principles?”
They work together. SRP creates small classes that are easy to close. LSP ensures your extensions won’t break existing behavior. DIP provides the abstractions OCP depends on.
Is your team wrestling with technical debt or looking to build a codebase that scales with AI-powered development? Clean Code Guy provides codebase audits, hands-on refactoring, and workshops to master principles like OCP. Learn how we can help you ship better software, faster: https://cleancodeguy.com
Q&A — concise user-focused questions and answers
Q: How do I spot an OCP violation in my codebase?
A: Look for long if/else or switch chains, large components that change when new variants are added, or base classes that frequently require edits. If one file changes every time you add a feature, that’s a sign.
Q: When should I refactor for OCP?
A: Refactor when code is repeatedly modified for new behavior. Start small: extract an interface or wrapper and move type-specific logic into new classes.
Q: What practical steps help a team adopt OCP?
A: Add OCP checks to code reviews, teach patterns like Strategy and composition, use linters to detect anti-patterns, and run focused workshops with real code examples.
AI writes code.You make it last.
In the age of AI acceleration, clean code isn’t just good practice — it’s the difference between systems that scale and codebases that collapse under their own weight.