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.
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
Related Patterns
- 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