DEV Community

Diego Liascovich
Diego Liascovich

Posted on

Managing Dynamic Behavior with the State Design Pattern in TypeScript

By Diego Liascovich

Full-Stack Developer | Microservices | Angular | Node.js

πŸ“˜ What is the State Design Pattern?

The State Pattern is a behavioral design pattern that lets an object alter its behavior when its internal state changes. The object will appear to change its class.

Rather than using conditionals, each state is encapsulated in a separate class. The object delegates behavior to the current state class.


🧱 Key Components

  • Context: Holds a reference to the current state and delegates operations.
  • State Interface: Defines the common interface for all states.
  • Concrete States: Represent different states with unique behavior.

πŸ’‘ Real Case: Billing Invoice Lifecycle

A billing invoice can go through different states:

  • Created
  • Approved
  • Paid
  • Canceled

Each state defines what actions are valid. For example:

  • A Created invoice can be approved or canceled.
  • An Approved invoice can be paid or canceled.
  • A Paid invoice cannot be modified.
  • A Canceled invoice is read-only.

πŸ› οΈ TypeScript Implementation

1. State Interface

// BillingState.ts
export interface BillingState {
  approve(): void;
  pay(): void;
  cancel(): void;
}
Enter fullscreen mode Exit fullscreen mode

2. Concrete States

// CreatedState.ts
import { BillingState } from './BillingState';
import { BillingContext } from './BillingContext';
import { ApprovedState } from './ApprovedState';
import { CanceledState } from './CanceledState';

export class CreatedState implements BillingState {
  constructor(private context: BillingContext) {}

  approve(): void {
    console.log("Invoice approved.");
    this.context.setState(new ApprovedState(this.context));
  }

  pay(): void {
    console.log("Cannot pay a created invoice.");
  }

  cancel(): void {
    console.log("Invoice canceled.");
    this.context.setState(new CanceledState(this.context));
  }
}
Enter fullscreen mode Exit fullscreen mode
// ApprovedState.ts
import { BillingState } from './BillingState';
import { BillingContext } from './BillingContext';
import { PaidState } from './PaidState';
import { CanceledState } from './CanceledState';

export class ApprovedState implements BillingState {
  constructor(private context: BillingContext) {}

  approve(): void {
    console.log("Already approved.");
  }

  pay(): void {
    console.log("Invoice paid.");
    this.context.setState(new PaidState(this.context));
  }

  cancel(): void {
    console.log("Invoice canceled.");
    this.context.setState(new CanceledState(this.context));
  }
}
Enter fullscreen mode Exit fullscreen mode
// PaidState.ts
import { BillingState } from './BillingState';

export class PaidState implements BillingState {
  approve(): void {
    console.log("Invoice already paid. Cannot approve.");
  }

  pay(): void {
    console.log("Invoice already paid.");
  }

  cancel(): void {
    console.log("Invoice already paid. Cannot cancel.");
  }
}
Enter fullscreen mode Exit fullscreen mode
// CanceledState.ts
import { BillingState } from './BillingState';

export class CanceledState implements BillingState {
  approve(): void {
    console.log("Invoice is canceled. Cannot approve.");
  }

  pay(): void {
    console.log("Invoice is canceled. Cannot pay.");
  }

  cancel(): void {
    console.log("Invoice already canceled.");
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Context Class

// BillingContext.ts
import { BillingState } from './BillingState';
import { CreatedState } from './CreatedState';

export class BillingContext {
  private state: BillingState;

  constructor() {
    this.state = new CreatedState(this); // initial state
  }

  setState(state: BillingState) {
    this.state = state;
  }

  approve() {
    this.state.approve();
  }

  pay() {
    this.state.pay();
  }

  cancel() {
    this.state.cancel();
  }
}
Enter fullscreen mode Exit fullscreen mode

βœ… Demo Usage

const invoice = new BillingContext();

invoice.approve(); // "Invoice approved."
invoice.pay();     // "Invoice paid."
invoice.cancel();  // "Invoice already paid. Cannot cancel."
Enter fullscreen mode Exit fullscreen mode

🎯 Benefits of Using the State Pattern for Billing

  • Enforces business rules via state logic.
  • Avoids complex conditionals.
  • Each state encapsulates its valid transitions and behavior.
  • Easy to extend with new states (e.g., "Refunded", "Disputed").

Top comments (0)