December 7, 2025 (8mo ago) — last updated July 2, 2026 (1mo ago)

Open-Closed Principle: TypeScript & React

Apply OCP in TypeScript and React using interfaces, composition, and Strategy to build extensible, maintainable systems.

← Back to blog
Cover Image for Open-Closed Principle: TypeScript & React

The Open‑Closed Principle (OCP) states software should be open for extension but closed for modification. This guide shows practical TypeScript and React patterns—interfaces, composition, and Strategy—to add features safely without changing proven code.

Open-Closed Principle: TypeScript & React

Summary: Apply the Open‑Closed Principle in TypeScript and React using interfaces, composition, and the Strategy pattern to build extensible, maintainable code.

Introduction

The Open‑Closed Principle (OCP) states that software should be open for extension but closed for modification. In TypeScript and React, following OCP helps teams add features without changing proven code, reducing regressions and speeding delivery. This guide presents practical patterns—interfaces, composition, and the Strategy pattern—so you can evolve systems safely and predictably.

The Open‑Closed Principle in plain terms

OCP can sound academic, but it’s highly practical: design modules so you add behavior by introducing new code rather than 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 and 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 the Single Responsibility Principle and Dependency Inversion. When teams adopt OCP, they reduce maintenance churn and keep core systems stable while adding features rapidly.1

“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 does not change. See related patterns: Strategy pattern and Dependency Injection.

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. See also React composition patterns.

// 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. Design and modular practices help teams adapt to changing requirements and tooling environments.2

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, increasing time spent on bug fixes and small enhancements rather than new features.1

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.

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. If a file rarely changes, don’t abstract it prematurely.

“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‑assisted 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

Concise Q&A — Practical troubleshooting

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.

Three concise Q&A sections (bottom of article)

Q: What’s the quickest way to apply OCP?

A: Depend on interfaces and inject behavior via composition or strategy classes.

Q: How do I avoid over‑engineering for OCP?

A: Apply OCP where change is likely. If a file rarely changes, keep it simple.

Q: Which parts of my stack benefit most from OCP?

A: Integrations (payments, exports), UI variant rendering, and business‑rule engines benefit most.

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.