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
Cover Image for Open-Closed Principle Guide (TypeScript & React)

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.

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

An architectural drawing depicting a house interior with various networked devices and labeled connections.

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 hand-drawn software diagram illustrates Interface, Concrete, and Indirect classes with dependencies.

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.

Hand-drawn concept showing a UI design evolving from a static layout to a dynamic, modular one.

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

ScenarioCode Violating OCP (Before)Code Adhering to OCP (After)
Rendering UI variantsSingle component with a large switch on typeGeneric container accepts specialized components as children
Data export logicexportData(format, data) with if/else blocksExporter interface with CsvExporter, JsonExporter classes
Discount calculationcalculateDiscount with many if chainsDiscountStrategy interface with concrete strategies
Handling user actionsMassive reducer with one switch on action.typeSmaller reducers, composition, middleware
Backend routingOne routing file with dozens of routesModular 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

Hand-drawn diagrams illustrating OCP mistakes and a refactored strategy pattern, with various boxes and arrows.

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 switch or long if/else likely 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.

1.
Sean Carpenter, “The Cost of Software Maintenance,” SEI Blog, Carnegie Mellon University Software Engineering Institute, June 4, 2018, https://insights.sei.cmu.edu/sei_blog/2018/06/the-cost-of-software-maintenance.html.
2.
Stanford University, “AI Index Report,” https://aiindex.stanford.edu/report/.
3.
California Department of Education, Digital Services Principles and Guidance, https://www.cde.ca.gov/ta/ac/cm/dbprinciples.asp.
← Back to blog
🙋🏻‍♂️

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.