State

Let an object alter its behaviour when its internal state changes. The object appears to change its class. Instead of giant switch/if-else blocks, extract each state into its own class.

The Problem

You’re modelling a vending machine. It has states: idle, hasCoin, dispensing, outOfStock. Each state allows different actions. An naive implementation:

class VendingMachine {
  state: 'idle' | 'hasCoin' | 'dispensing' | 'outOfStock' = 'idle';

  insertCoin(): void {
    if (this.state === 'idle') { this.state = 'hasCoin'; ... }
    else if (this.state === 'hasCoin') { ... }
    else if (this.state === 'outOfStock') { ... }
    // grows with every new state
  }

  selectItem(): void {
    if (this.state === 'hasCoin') { ... }
    // ... more branches
  }
}

Every method has a switch/if-else covering all states. Add a new state: edit every method. This is hard to maintain and violates Open/Closed.


The Solution

Extract each state into a class. The Context (VendingMachine) holds a reference to the current state object and delegates behaviour to it. State transitions happen by replacing the state object.

Context - state: State + request() «interface» State + handle(ctx: Context) StateA + handle(ctx) StateB + handle(ctx) transition
State — Context delegates to current State object; transitions happen by swapping the state reference

Real-World Analogy

A traffic light. The light has a state (red, yellow, green). When it transitions to green, “car behaviour” changes from “stop” to “go.” The traffic light doesn’t conditionally check its own colour inside each method — it delegates to the current state.


TypeScript Example

// State interface
interface VendingState {
  insertCoin(machine: VendingMachine): void;
  selectItem(machine: VendingMachine, item: string): void;
  cancelTransaction(machine: VendingMachine): void;
}

// Context
class VendingMachine {
  private inventory = new Map<string, number>([
    ['Cola',  3],
    ['Water', 5],
    ['Chips', 2],
  ]);
  private balance = 0;

  constructor(private state: VendingState = new IdleState()) {}

  // Let states transition us
  setState(state: VendingState): void {
    console.log(`  [Machine] State → ${state.constructor.name}`);
    this.state = state;
  }

  insertCoin():                     void { this.state.insertCoin(this); }
  selectItem(item: string):         void { this.state.selectItem(this, item); }
  cancelTransaction():              void { this.state.cancelTransaction(this); }

  addBalance(amount: number):       void { this.balance += amount; }
  getBalance():                  number { return this.balance; }
  resetBalance():                   void { this.balance = 0; }

  hasItem(item: string):         boolean { return (this.inventory.get(item) ?? 0) > 0; }
  dispenseItem(item: string):       void {
    const count = this.inventory.get(item) ?? 0;
    this.inventory.set(item, count - 1);
    console.log(`  [Machine] Dispensed: ${item}`);
    const total = [...this.inventory.values()].reduce((a, b) => a + b, 0);
    if (total === 0) this.setState(new OutOfStockState());
    else             this.setState(new IdleState());
  }
}

// Concrete states
class IdleState implements VendingState {
  insertCoin(machine: VendingMachine): void {
    machine.addBalance(1);
    console.log(`  Balance: $${machine.getBalance()}`);
    machine.setState(new HasCoinState());
  }
  selectItem(machine: VendingMachine): void {
    console.log('  Please insert a coin first.');
  }
  cancelTransaction(machine: VendingMachine): void {
    console.log('  Nothing to cancel.');
  }
}

class HasCoinState implements VendingState {
  insertCoin(machine: VendingMachine): void {
    machine.addBalance(1);
    console.log(`  Balance: $${machine.getBalance()}`);
  }
  selectItem(machine: VendingMachine, item: string): void {
    if (!machine.hasItem(item)) {
      console.log(`  "${item}" is out of stock.`);
      return;
    }
    console.log(`  Dispensing ${item}...`);
    machine.setState(new DispensingState());
    machine.dispenseItem(item);
    machine.resetBalance();
  }
  cancelTransaction(machine: VendingMachine): void {
    console.log(`  Refunding $${machine.getBalance()}`);
    machine.resetBalance();
    machine.setState(new IdleState());
  }
}

class DispensingState implements VendingState {
  insertCoin(machine: VendingMachine): void {
    console.log('  Please wait, dispensing in progress.');
  }
  selectItem(machine: VendingMachine): void {
    console.log('  Please wait, dispensing in progress.');
  }
  cancelTransaction(machine: VendingMachine): void {
    console.log('  Cannot cancel while dispensing.');
  }
}

class OutOfStockState implements VendingState {
  insertCoin(machine: VendingMachine): void {
    console.log('  Machine is out of stock. Returning coin.');
  }
  selectItem(machine: VendingMachine): void {
    console.log('  Machine is out of stock.');
  }
  cancelTransaction(machine: VendingMachine): void {
    console.log('  Nothing to cancel.');
  }
}

// Demo
const machine = new VendingMachine();
console.log('--- Select without coin ---');
machine.selectItem('Cola');

console.log('\n--- Insert coin + select ---');
machine.insertCoin();
machine.selectItem('Cola');

console.log('\n--- Cancel ---');
machine.insertCoin();
machine.cancelTransaction();

When to Use

  • An object’s behaviour depends on its state and must change at runtime
  • You have large conditional statements that check the object’s state (switch/if-else on a status enum)
  • State transitions need to be clearly defined and easy to add/change
  • Examples: order processing (pending → paid → shipped → delivered), traffic lights, media players (playing/paused/stopped), auth flows, game character states

Pros

  • Eliminates large conditional blocks by distributing behaviour into state classes
  • Open/Closed — add new states without touching the context or other state classes
  • Transitions are explicit — each state knows what states it can transition to
  • Each state class has a clear, focused responsibility

Cons

  • Can be overkill for objects with only two or three states
  • Increases the number of classes

  • Strategy is structurally similar — both swap the active “policy” object — but State transitions itself; Strategy is swapped by the client
  • Flyweight can be used when many contexts share the same state objects
  • Singleton — state objects with no instance data can be shared as Singletons