December 20, 2025 (8mo ago) — last updated June 21, 2026 (2mo ago)

TypeScriptで学ぶ OOPデザインパターン チュートリアル

TypeScriptの実践コードで主要なOOPデザインパターン(生成・構造・振る舞い)を解説。設計力と保守性を高める実用ガイド。

← Back to blog
Cover Image for TypeScriptで学ぶ OOPデザインパターン チュートリアル

Design patterns are proven, reusable templates that make object-oriented code easier to understand, test, and extend. This guide uses compact TypeScript examples to teach creational, structural, and behavioral patterns so you can write clearer, more maintainable code.

TypeScriptで学ぶ OOPデザインパターン

Master object-oriented design patterns with clear TypeScript examples. Learn creational, structural, and behavioral patterns to improve design, testability, and maintainability.

Introduction

Design patterns are proven, reusable templates for solving recurring design problems in object-oriented code. They’re adaptable blueprints—not copy-and-paste snippets—that help you structure classes and objects so code is easier to maintain, extend, and test. A practical understanding of creational, structural, and behavioral patterns helps you communicate design intent, refactor safely, and make better architectural choices.

A focused set of patterns unlocks faster reviews, fewer bugs, and clearer code. Classic references document the long-term benefits of applying patterns thoughtfully, and the Gang of Four catalog remains the standard reference for the 23 foundational patterns1. Research into pattern abstraction and reuse supports these benefits2. Software maintenance can consume a large share of a system’s lifecycle cost—often cited as 60–80%—so investing in clear design early has long-term returns3.

What are design patterns in OOP?

Coding without patterns can produce tangled code that’s hard to scale. Patterns are like a cookbook of tested recipes you adapt to solve similar problems consistently. Knowing patterns helps you spot opportunities to simplify code, reduce duplication, and lower maintenance cost.

A sketch of a chef’s hat dropping golden letter shapes into an open cookbook.

Shared language and faster collaboration

Patterns give teams a shared vocabulary. Say “Factory” or “Singleton” and experienced developers immediately understand intent and high-level structure, which speeds communication and architecture decisions. For a practical primer on related OOP principles, see our guide comparing polymorphism vs inheritance: https://cleancodeguy.com/blog/polymorphism-vs-inheritance

The three core categories of design patterns

The Gang of Four grouped patterns into three categories: creational, structural, and behavioral. Recognizing these categories helps you pick the right approach for a problem1.

Diagram illustrating creational, structural, and behavioural design patterns with simple box illustrations.

Overview of pattern categories

Pattern categoryCore purposeCommon examples
CreationalAbstract and manage object creationFactory, Builder, Singleton, Prototype
StructuralCompose objects into larger, flexible structuresAdapter, Decorator, Facade, Composite
BehavioralDefine object interaction and communicationObserver, Strategy, Command, Iterator

Creational patterns

Creational patterns control how objects are created so client code isn’t tightly coupled to concrete classes. They hide creation logic and increase flexibility.

Structural patterns

Structural patterns help you assemble objects into larger systems and simplify relationships so parts can change without breaking the whole.

Behavioral patterns

Behavioral patterns shape communication and responsibility. They reduce dependencies and make systems easier to reason about.

Creational patterns in TypeScript

Creational patterns introduce an abstraction layer around object creation so you can swap implementations without changing client code.

An illustration contrasting individual egg-like objects with a factory assembly line producing colorful items.

Singleton — one true instance

Singleton ensures a class has only one instance and provides a global access point. It’s useful for shared resources like a database connection or logger, but it can introduce global state that complicates testing and dependency management4.

Example: a TypeScript Singleton for a database connection.

class DatabaseConnection {
  private static instance: DatabaseConnection;

  private constructor() {
    // A private constructor prevents external ’new’ calls
    console.log("Connecting to the database...");
  }

  public static getInstance(): DatabaseConnection {
    if (!DatabaseConnection.instance) {
      DatabaseConnection.instance = new DatabaseConnection();
    }
    return DatabaseConnection.instance;
  }

  public query(sql: string): void {
    console.log(`Executing query: ${sql}`);
  }
}

// Usage
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();

db1.query("SELECT * FROM users");
console.log(db1 === db2); // true

Use Singletons for truly global resources. Prefer dependency injection for better testability and modularity when possible4.

Factory Method — let subclasses decide

Factory Method defines an interface for creating an object while subclasses decide which concrete product to instantiate. It decouples clients from concrete classes, making the system easier to extend.

Example: rendering OS-specific buttons in TypeScript.

interface Button {
  render(): void;
  onClick(f: () => void): void;
}

class WindowsButton implements Button {
  render() { console.log("Rendering a button in Windows style."); }
  onClick(f: () => void) { console.log("Windows button click event."); f(); }
}

class MacButton implements Button {
  render() { console.log("Rendering a button in macOS style."); }
  onClick(f: () => void) { console.log("Mac button click event."); f(); }
}

abstract class Dialog {
  abstract createButton(): Button;

  render() {
    const okButton = this.createButton();
    okButton.render();
  }
}

class WindowsDialog extends Dialog {
  createButton(): Button { return new WindowsButton(); }
}

class MacDialog extends Dialog {
  createButton(): Button { return new MacButton(); }
}

// Client
const os: string = "windows";
let dialog: Dialog;
if (os === "windows") dialog = new WindowsDialog(); else dialog = new MacDialog();
dialog.render();

Factory Method keeps the creator unaware of concrete products, so adding a LinuxDialog is straightforward.

Structural patterns: adapters and decorators

Structural patterns help you compose objects into robust, adaptable systems.

Hand-drawn illustrations of an electrical adapter and a stack of rings representing OOP design patterns.

Adapter — bridge incompatible interfaces

Adapter wraps an incompatible interface so it conforms to what your system expects. This avoids invasive changes to legacy code.

Example: adapting a ModernLogger to an existing ILogger interface.

class ModernLogger {
  public logInfo(message: string): void {
    console.log(`[INFO]: ${message}`);
  }
}

interface ILogger { log(message: string): void; }

class LoggerAdapter implements ILogger {
  private modernLogger: ModernLogger;
  constructor() { this.modernLogger = new ModernLogger(); }
  public log(message: string): void { this.modernLogger.logInfo(message); }
}

const logger: ILogger = new LoggerAdapter();
logger.log("User logged in successfully.");

Decorator — add functionality dynamically

Decorator adds responsibilities to objects at runtime by wrapping them. It’s more flexible than subclassing and follows the Single Responsibility Principle.

Example: composing a subscription with optional add-ons.

interface Subscription { getDescription(): string; getCost(): number; }

class BasicSubscription implements Subscription {
  getDescription(): string { return "Basic Plan"; }
  getCost(): number { return 10; }
}

abstract class SubscriptionDecorator implements Subscription {
  protected subscription: Subscription;
  constructor(subscription: Subscription) { this.subscription = subscription; }
  abstract getDescription(): string;
  abstract getCost(): number;
}

class PremiumSupportDecorator extends SubscriptionDecorator {
  getDescription(): string { return `${this.subscription.getDescription()}, Premium Support`; }
  getCost(): number { return this.subscription.getCost() + 5; }
}

class CloudStorageDecorator extends SubscriptionDecorator {
  getDescription(): string { return `${this.subscription.getDescription()}, 1TB Cloud Storage`; }
  getCost(): number { return this.subscription.getCost() + 7; }
}

let mySubscription: Subscription = new BasicSubscription();
mySubscription = new PremiumSupportDecorator(mySubscription);
mySubscription = new CloudStorageDecorator(mySubscription);
console.log(mySubscription.getDescription());
console.log(mySubscription.getCost());

This compositional approach keeps features modular and easy to test.

Behavioral patterns: observer and strategy

Behavioral patterns organize object communication so systems remain flexible and maintainable.

Observer — notify interested parties

Observer sets up a one-to-many relationship so when a Subject changes state, Observers are notified automatically. It’s ideal for event-driven systems.

Example: a simple notification service.

interface Subject { attach(observer: Observer): void; detach(observer: Observer): void; notify(): void; }
interface Observer { update(subject: Subject): void; }

class NotificationService implements Subject {
  public state: string = '';
  private observers: Observer[] = [];
  attach(observer: Observer): void { this.observers.push(observer); }
  detach(observer: Observer): void { const i = this.observers.indexOf(observer); if (i !== -1) this.observers.splice(i, 1); }
  notify(): void { for (const o of this.observers) o.update(this); }
  public createNewPost(title: string): void { this.state = `New Post: ${title}`; console.log(`\nNotificationService: A new post was created.`); this.notify(); }
}

class EmailNotifier implements Observer { public update(subject: Subject): void { if (subject instanceof NotificationService) console.log(`EmailNotifier: Sending email about "${subject.state}"`); } }
class PushNotifier implements Observer { public update(subject: Subject): void { if (subject instanceof NotificationService) console.log(`PushNotifier: Sending push notification for "${subject.state}"`); } }

const notificationService = new NotificationService();
const emailer = new EmailNotifier();
const pusher = new PushNotifier();
notificationService.attach(emailer);
notificationService.attach(pusher);
notificationService.createNewPost("Understanding Observer Pattern");
notificationService.detach(pusher);
notificationService.createNewPost("Why Strategy is Awesome");

Observer produces a loosely coupled design: Subjects don’t need to know concrete Observers they notify.

Strategy — encapsulate algorithms

Strategy lets you swap algorithms at runtime, avoiding large conditional blocks. It aligns with the Open/Closed Principle by allowing new strategies without modifying existing code5.

Example: payment strategies for a shopping cart.

interface PaymentStrategy { pay(amount: number): void; }
class CreditCardStrategy implements PaymentStrategy { pay(amount: number): void { console.log(`Paying $${amount} with Credit Card.`); } }
class PayPalStrategy implements PaymentStrategy { pay(amount: number): void { console.log(`Paying $${amount} via PayPal.`); } }

class ShoppingCart {
  private paymentStrategy: PaymentStrategy;
  constructor(strategy: PaymentStrategy) { this.paymentStrategy = strategy; }
  public setPaymentStrategy(strategy: PaymentStrategy) { this.paymentStrategy = strategy; }
  public checkout(amount: number): void { this.paymentStrategy.pay(amount); }
}

const cart = new ShoppingCart(new CreditCardStrategy());
cart.checkout(150);
cart.setPaymentStrategy(new PayPalStrategy());
cart.checkout(150);

Strategy removes conditional complexity and makes systems easier to extend.

Common pitfalls and refactoring strategies

Choosing a pattern is only half the battle. Avoid anti-patterns like the God Object, which hoards responsibilities and violates the Single Responsibility Principle. Look for code smells—long methods, excessive dependencies, or monster classes—and address them with disciplined refactoring.

Refactoring improves internal structure without changing external behavior. It’s how you tame legacy systems safely.

Refactor long conditionals to Strategy

A long switch or if/else chain is a classic smell. Extract the varying behavior into a Strategy interface with concrete strategy classes and replace the conditional with a single call to the selected strategy. This improves extensibility and testability5.


At Clean Code Guy, we help teams build software that lasts by embedding foundational principles into their workflow. Discover how our code audits and AI-ready refactoring can get your team shipping with confidence at https://cleancodeguy.com.

FAQ — Quick answers

Q: Which patterns should I learn first?

A: Start with Singleton, Factory, Adapter, Decorator, Observer, and Strategy. They solve common problems and are easy to recognize.

Q: When should I avoid using a pattern?

A: Avoid patterns that add complexity without solving a clear problem. Follow YAGNI and prefer simple, well-tested solutions first.

Q: How can I practice these patterns in TypeScript?

A: Build small features—logging, UI components, payment flows—using the patterns above and write unit tests for each variation to make refactoring safe.

Additional concise Q&A

Q: How do patterns improve maintainability?

A: Patterns make intent explicit and reduce ad-hoc coupling, which lowers maintenance effort and makes tests clearer. Well-structured code is easier to change and review.

Q: When is dependency injection better than Singleton?

A: Use dependency injection when you need testability and flexibility. Singletons create global state that can complicate unit tests and lifecycle control.

Q: How do I choose between Adapter and Facade?

A: Use Adapter to wrap an incompatible interface for one component; use Facade to provide a simplified API over a subsystem.

1.
Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994). https://en.wikipedia.org/wiki/Design_Patterns
2.
W. G. Griswold et al., “A Formal Foundation for Design Pattern Abstraction and Reuse,” Proceedings of ECOOP ’93, University of California, San Diego. https://cseweb.ucsd.edu/~wgg/CSE210/ecoop93-patterns.pdf
3.
“Software maintenance,” Wikipedia, notes that maintenance often represents the majority of lifecycle costs and can consume 60–80% of total lifecycle effort. https://en.wikipedia.org/wiki/Software_maintenance
4.
Martin Fowler, “Singleton,” Bliki. Discusses Singleton trade-offs and testing implications. https://martinfowler.com/bliki/Singleton.html
5.
Robert C. Martin (Uncle Bob), “The SOLID Principles.” https://8thlight.com/blog/uncle-bob/2012/08/13/the-s-o-l-i-d-principles.html
← Back to blog
🙋🏻‍♂️

AIがコードを書きます。
あなたがそれを長持ちさせます。

AI加速の時代において、クリーンコードは単なる良い実践ではありません—スケールするシステムと自らの重みで崩壊するコードベースの違いです。