Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ State Pattern: Behavioral States
๐ฏ Introduction
Ever played a video game where your character can walk, run, jump, or swim? ๐ฎ Each of these is a different state, and your character behaves differently in each one. Thatโs exactly what the State Pattern is all about โ managing objects that change their behavior based on their internal state!
The State Pattern is like a chameleon ๐ฆ that changes its behavior based on its environment. Instead of having massive if-else statements checking conditions everywhere, we elegantly encapsulate each stateโs behavior in its own class. Letโs dive in and make your code more flexible and maintainable! ๐ช
๐ Understanding State Pattern
The State Pattern allows an object to alter its behavior when its internal state changes. Think of it like a traffic light ๐ฆ:
- Red state: Cars stop, pedestrians can walk
- Yellow state: Cars prepare to stop, pedestrians wait
- Green state: Cars go, pedestrians must wait
Each state has its own set of behaviors, and the light transitions between states based on specific rules. Hereโs the beauty: instead of one massive class handling all possible states, we create separate classes for each state! ๐จ
Core Components:
- Context: The main object whose behavior changes (the traffic light itself)
- State Interface: Defines methods all states must implement
- Concrete States: Individual state implementations (Red, Yellow, Green)
- State Transitions: Rules for moving between states
๐ง Basic Syntax and Usage
Letโs start with a simple example โ a music player that can be in different states! ๐ต
// ๐ฏ State interface
interface PlayerState {
play(): void;
pause(): void;
stop(): void;
}
// ๐ฎ Context class
class MusicPlayer {
private state: PlayerState;
constructor() {
// ๐ Start in stopped state
this.state = new StoppedState(this);
}
setState(state: PlayerState): void {
this.state = state;
}
// ๐ต Delegate actions to current state
play(): void {
this.state.play();
}
pause(): void {
this.state.pause();
}
stop(): void {
this.state.stop();
}
}
// ๐ Stopped state
class StoppedState implements PlayerState {
constructor(private player: MusicPlayer) {}
play(): void {
console.log("โถ๏ธ Starting playback!");
this.player.setState(new PlayingState(this.player));
}
pause(): void {
console.log("โ ๏ธ Can't pause - already stopped!");
}
stop(): void {
console.log("๐ Already stopped!");
}
}
// ๐ต Playing state
class PlayingState implements PlayerState {
constructor(private player: MusicPlayer) {}
play(): void {
console.log("๐ต Already playing!");
}
pause(): void {
console.log("โธ๏ธ Pausing playback!");
this.player.setState(new PausedState(this.player));
}
stop(): void {
console.log("โน๏ธ Stopping playback!");
this.player.setState(new StoppedState(this.player));
}
}
// โธ๏ธ Paused state
class PausedState implements PlayerState {
constructor(private player: MusicPlayer) {}
play(): void {
console.log("โถ๏ธ Resuming playback!");
this.player.setState(new PlayingState(this.player));
}
pause(): void {
console.log("โธ๏ธ Already paused!");
}
stop(): void {
console.log("โน๏ธ Stopping from pause!");
this.player.setState(new StoppedState(this.player));
}
}
// ๐ช Let's use it!
const player = new MusicPlayer();
player.play(); // โถ๏ธ Starting playback!
player.pause(); // โธ๏ธ Pausing playback!
player.play(); // โถ๏ธ Resuming playback!
player.stop(); // โน๏ธ Stopping playback!
๐ก Practical Examples
Example 1: Smart Door Lock ๐ช
Letโs build a smart door lock system that can be locked, unlocked, or in alarm mode!
// ๐ Door lock states
interface DoorState {
lock(): void;
unlock(pin: string): void;
alarm(): void;
}
class SmartDoor {
private state: DoorState;
private correctPin = "1234";
private attempts = 0;
constructor() {
this.state = new LockedState(this);
}
setState(state: DoorState): void {
this.state = state;
}
getPin(): string {
return this.correctPin;
}
incrementAttempts(): void {
this.attempts++;
}
resetAttempts(): void {
this.attempts = 0;
}
getAttempts(): number {
return this.attempts;
}
// ๐ฏ Public methods
lock(): void {
this.state.lock();
}
unlock(pin: string): void {
this.state.unlock(pin);
}
alarm(): void {
this.state.alarm();
}
}
// ๐ Locked state
class LockedState implements DoorState {
constructor(private door: SmartDoor) {}
lock(): void {
console.log("๐ Door is already locked!");
}
unlock(pin: string): void {
if (pin === this.door.getPin()) {
console.log("โ
Correct PIN! Door unlocked! ๐");
this.door.resetAttempts();
this.door.setState(new UnlockedState(this.door));
} else {
this.door.incrementAttempts();
console.log(`โ Wrong PIN! Attempt ${this.door.getAttempts()}/3`);
if (this.door.getAttempts() >= 3) {
console.log("๐จ Too many attempts! ALARM!");
this.door.setState(new AlarmState(this.door));
}
}
}
alarm(): void {
console.log("๐จ Triggering alarm!");
this.door.setState(new AlarmState(this.door));
}
}
// ๐ Unlocked state
class UnlockedState implements DoorState {
constructor(private door: SmartDoor) {}
lock(): void {
console.log("๐ Locking door!");
this.door.setState(new LockedState(this.door));
}
unlock(pin: string): void {
console.log("๐ Door is already unlocked!");
}
alarm(): void {
console.log("๐จ Triggering alarm!");
this.door.setState(new AlarmState(this.door));
}
}
// ๐จ Alarm state
class AlarmState implements DoorState {
constructor(private door: SmartDoor) {}
lock(): void {
console.log("โ ๏ธ Cannot lock - alarm is active!");
}
unlock(pin: string): void {
if (pin === this.door.getPin()) {
console.log("โ
Alarm disabled! Door unlocked! ๐");
this.door.resetAttempts();
this.door.setState(new UnlockedState(this.door));
} else {
console.log("โ Wrong PIN! Alarm still active! ๐จ");
}
}
alarm(): void {
console.log("๐จ Alarm is already active!");
}
}
// ๐ฎ Let's test our smart door!
const door = new SmartDoor();
door.unlock("0000"); // โ Wrong PIN! Attempt 1/3
door.unlock("1111"); // โ Wrong PIN! Attempt 2/3
door.unlock("2222"); // โ Wrong PIN! Attempt 3/3
// ๐จ Too many attempts! ALARM!
door.lock(); // โ ๏ธ Cannot lock - alarm is active!
door.unlock("1234"); // โ
Alarm disabled! Door unlocked! ๐
door.lock(); // ๐ Locking door!
Example 2: Vending Machine ๐ฅค
Letโs create a vending machine with different states!
// ๐ฅค Vending machine states
interface VendingState {
insertCoin(): void;
selectProduct(): void;
dispense(): void;
refund(): void;
}
class VendingMachine {
private state: VendingState;
private balance = 0;
private productPrice = 2.50;
constructor() {
this.state = new IdleState(this);
}
setState(state: VendingState): void {
this.state = state;
}
addBalance(amount: number): void {
this.balance += amount;
console.log(`๐ฐ Balance: $${this.balance.toFixed(2)}`);
}
getBalance(): number {
return this.balance;
}
getProductPrice(): number {
return this.productPrice;
}
resetBalance(): void {
this.balance = 0;
}
// ๐ฏ Public methods
insertCoin(): void {
this.state.insertCoin();
}
selectProduct(): void {
this.state.selectProduct();
}
dispense(): void {
this.state.dispense();
}
refund(): void {
this.state.refund();
}
}
// ๐ Idle state
class IdleState implements VendingState {
constructor(private machine: VendingMachine) {}
insertCoin(): void {
console.log("๐ช Coin inserted!");
this.machine.addBalance(1.00);
this.machine.setState(new HasMoneyState(this.machine));
}
selectProduct(): void {
console.log("โ Please insert money first!");
}
dispense(): void {
console.log("โ Please insert money and select a product!");
}
refund(): void {
console.log("๐ธ Nothing to refund!");
}
}
// ๐ฐ Has money state
class HasMoneyState implements VendingState {
constructor(private machine: VendingMachine) {}
insertCoin(): void {
console.log("๐ช Another coin inserted!");
this.machine.addBalance(1.00);
}
selectProduct(): void {
if (this.machine.getBalance() >= this.machine.getProductPrice()) {
console.log("โ
Product selected! Dispensing...");
this.machine.setState(new DispensingState(this.machine));
this.machine.dispense();
} else {
const needed = this.machine.getProductPrice() - this.machine.getBalance();
console.log(`โ Not enough money! Need $${needed.toFixed(2)} more`);
}
}
dispense(): void {
console.log("โ Please select a product first!");
}
refund(): void {
console.log(`๐ธ Refunding $${this.machine.getBalance().toFixed(2)}`);
this.machine.resetBalance();
this.machine.setState(new IdleState(this.machine));
}
}
// ๐ฆ Dispensing state
class DispensingState implements VendingState {
constructor(private machine: VendingMachine) {}
insertCoin(): void {
console.log("โณ Please wait... dispensing product!");
}
selectProduct(): void {
console.log("โณ Already dispensing a product!");
}
dispense(): void {
console.log("๐ฅค Here's your drink! Enjoy! ๐");
const change = this.machine.getBalance() - this.machine.getProductPrice();
if (change > 0) {
console.log(`๐ฐ Your change: $${change.toFixed(2)}`);
}
this.machine.resetBalance();
this.machine.setState(new IdleState(this.machine));
}
refund(): void {
console.log("โณ Cannot refund while dispensing!");
}
}
// ๐ฎ Let's buy a drink!
const vendingMachine = new VendingMachine();
vendingMachine.selectProduct(); // โ Please insert money first!
vendingMachine.insertCoin(); // ๐ช Coin inserted! ๐ฐ Balance: $1.00
vendingMachine.insertCoin(); // ๐ช Another coin inserted! ๐ฐ Balance: $2.00
vendingMachine.selectProduct(); // โ Not enough money! Need $0.50 more
vendingMachine.insertCoin(); // ๐ช Another coin inserted! ๐ฐ Balance: $3.00
vendingMachine.selectProduct(); // โ
Product selected! Dispensing...
// ๐ฅค Here's your drink! Enjoy! ๐
// ๐ฐ Your change: $0.50
๐ Advanced Concepts
State with Context Data ๐
Sometimes states need to carry data between transitions:
// ๐ฎ Game character with stats
interface CharacterState {
move(): void;
attack(): void;
takeDamage(damage: number): void;
heal(): void;
}
class GameCharacter {
private state: CharacterState;
private health = 100;
private maxHealth = 100;
constructor(public name: string) {
this.state = new HealthyState(this);
}
setState(state: CharacterState): void {
this.state = state;
}
getHealth(): number {
return this.health;
}
setHealth(health: number): void {
this.health = Math.max(0, Math.min(health, this.maxHealth));
console.log(`โค๏ธ ${this.name}'s health: ${this.health}/${this.maxHealth}`);
// ๐ Auto-transition based on health
if (this.health === 0) {
this.setState(new DefeatedState(this));
} else if (this.health <= 30) {
this.setState(new CriticalState(this));
} else if (this.health <= 60) {
this.setState(new InjuredState(this));
} else {
this.setState(new HealthyState(this));
}
}
// ๐ฏ Actions
move(): void { this.state.move(); }
attack(): void { this.state.attack(); }
takeDamage(damage: number): void { this.state.takeDamage(damage); }
heal(): void { this.state.heal(); }
}
// ๐ช Healthy state
class HealthyState implements CharacterState {
constructor(private character: GameCharacter) {}
move(): void {
console.log(`๐ ${this.character.name} moves swiftly!`);
}
attack(): void {
console.log(`โ๏ธ ${this.character.name} attacks with full power!`);
}
takeDamage(damage: number): void {
console.log(`๐ฅ ${this.character.name} takes ${damage} damage!`);
this.character.setHealth(this.character.getHealth() - damage);
}
heal(): void {
console.log(`โจ ${this.character.name} heals!`);
this.character.setHealth(this.character.getHealth() + 30);
}
}
// ๐ค Injured state
class InjuredState implements CharacterState {
constructor(private character: GameCharacter) {}
move(): void {
console.log(`๐ถ ${this.character.name} limps slowly...`);
}
attack(): void {
console.log(`๐ก๏ธ ${this.character.name} attacks weakly`);
}
takeDamage(damage: number): void {
console.log(`๐ฅ ${this.character.name} takes ${damage} damage! Ouch!`);
this.character.setHealth(this.character.getHealth() - damage);
}
heal(): void {
console.log(`๐ ${this.character.name} desperately heals!`);
this.character.setHealth(this.character.getHealth() + 30);
}
}
// ๐ Critical state
class CriticalState implements CharacterState {
constructor(private character: GameCharacter) {}
move(): void {
console.log(`๐ ${this.character.name} can barely move!`);
}
attack(): void {
console.log(`๐ฐ ${this.character.name} is too weak to attack!`);
}
takeDamage(damage: number): void {
console.log(`๐ฅ ${this.character.name} takes ${damage} critical damage!`);
this.character.setHealth(this.character.getHealth() - damage);
}
heal(): void {
console.log(`๐ ${this.character.name} uses emergency healing!`);
this.character.setHealth(this.character.getHealth() + 40);
}
}
// ๐ Defeated state
class DefeatedState implements CharacterState {
constructor(private character: GameCharacter) {}
move(): void {
console.log(`โ ๏ธ ${this.character.name} cannot move... defeated!`);
}
attack(): void {
console.log(`โ ๏ธ ${this.character.name} cannot attack... defeated!`);
}
takeDamage(damage: number): void {
console.log(`โ ๏ธ ${this.character.name} is already defeated!`);
}
heal(): void {
console.log(`๐ฎ ${this.character.name} is revived!`);
this.character.setHealth(50);
}
}
// ๐ฎ Epic battle!
const hero = new GameCharacter("Hero");
hero.attack(); // โ๏ธ Hero attacks with full power!
hero.takeDamage(50); // ๐ฅ Hero takes 50 damage! โค๏ธ Hero's health: 50/100
hero.move(); // ๐ถ Hero limps slowly...
hero.takeDamage(30); // ๐ฅ Hero takes 30 damage! Ouch! โค๏ธ Hero's health: 20/100
hero.attack(); // ๐ฐ Hero is too weak to attack!
hero.heal(); // ๐ Hero uses emergency healing! โค๏ธ Hero's health: 60/100
hero.move(); // ๐ถ Hero limps slowly...
Async State Transitions ๐
Real-world applications often need async operations:
// ๐ก Connection states with async operations
interface ConnectionState {
connect(): Promise<void>;
disconnect(): Promise<void>;
sendData(data: string): Promise<void>;
}
class NetworkConnection {
private state: ConnectionState;
constructor() {
this.state = new DisconnectedState(this);
}
setState(state: ConnectionState): void {
this.state = state;
}
// ๐ฏ Public async methods
async connect(): Promise<void> {
await this.state.connect();
}
async disconnect(): Promise<void> {
await this.state.disconnect();
}
async sendData(data: string): Promise<void> {
await this.state.sendData(data);
}
}
// ๐ Disconnected state
class DisconnectedState implements ConnectionState {
constructor(private connection: NetworkConnection) {}
async connect(): Promise<void> {
console.log("๐ Connecting...");
this.connection.setState(new ConnectingState(this.connection));
// ๐ฒ Simulate connection attempt
await new Promise(resolve => setTimeout(resolve, 1000));
const success = Math.random() > 0.3;
if (success) {
console.log("โ
Connected successfully!");
this.connection.setState(new ConnectedState(this.connection));
} else {
console.log("โ Connection failed!");
this.connection.setState(new DisconnectedState(this.connection));
}
}
async disconnect(): Promise<void> {
console.log("๐ Already disconnected!");
}
async sendData(data: string): Promise<void> {
console.log("โ Cannot send data - not connected!");
}
}
// ๐ Connecting state
class ConnectingState implements ConnectionState {
constructor(private connection: NetworkConnection) {}
async connect(): Promise<void> {
console.log("โณ Already connecting... please wait!");
}
async disconnect(): Promise<void> {
console.log("๐ Cancelling connection attempt!");
this.connection.setState(new DisconnectedState(this.connection));
}
async sendData(data: string): Promise<void> {
console.log("โณ Cannot send data - still connecting!");
}
}
// โ
Connected state
class ConnectedState implements ConnectionState {
constructor(private connection: NetworkConnection) {}
async connect(): Promise<void> {
console.log("๐ก Already connected!");
}
async disconnect(): Promise<void> {
console.log("๐ Disconnecting...");
await new Promise(resolve => setTimeout(resolve, 500));
console.log("๐ Disconnected!");
this.connection.setState(new DisconnectedState(this.connection));
}
async sendData(data: string): Promise<void> {
console.log(`๐ค Sending: "${data}"`);
await new Promise(resolve => setTimeout(resolve, 300));
console.log("โ
Data sent successfully!");
}
}
// ๐ฎ Let's test async states!
const network = new NetworkConnection();
(async () => {
await network.sendData("Hello"); // โ Cannot send data - not connected!
await network.connect(); // ๐ Connecting... โ
Connected successfully!
await network.sendData("Hello!"); // ๐ค Sending: "Hello!" โ
Data sent successfully!
await network.disconnect(); // ๐ Disconnecting... ๐ Disconnected!
})();
โ ๏ธ Common Pitfalls and Solutions
โ Wrong: State logic in context
// โ BAD: Context knows too much about states
class BadPlayer {
private state: string = "stopped";
play(): void {
if (this.state === "stopped") {
console.log("Playing...");
this.state = "playing";
} else if (this.state === "paused") {
console.log("Resuming...");
this.state = "playing";
} else {
console.log("Already playing!");
}
}
// ๐ฑ This gets messy fast!
}
โ Correct: Encapsulated state behavior
// โ
GOOD: States handle their own logic
class GoodPlayer {
private state: PlayerState;
constructor() {
this.state = new StoppedState(this);
}
setState(state: PlayerState): void {
this.state = state;
}
play(): void {
this.state.play(); // ๐ฏ Delegate to state!
}
}
โ Wrong: Forgetting state transitions
// โ BAD: State doesn't transition
class BrokenState implements PlayerState {
play(): void {
console.log("Playing!");
// ๐ฑ Forgot to change state!
}
pause(): void {
console.log("Can't pause!");
}
stop(): void {
console.log("Can't stop!");
}
}
โ Correct: Proper state transitions
// โ
GOOD: States transition properly
class WorkingState implements PlayerState {
constructor(private player: MusicPlayer) {}
play(): void {
console.log("โถ๏ธ Playing!");
this.player.setState(new PlayingState(this.player)); // ๐ฏ Transition!
}
pause(): void {
console.log("โ ๏ธ Can't pause from this state");
}
stop(): void {
console.log("โน๏ธ Stopping!");
this.player.setState(new StoppedState(this.player)); // ๐ฏ Transition!
}
}
๐ ๏ธ Best Practices
1. Keep States Focused ๐ฏ
Each state should have a single responsibility and clear transitions.
2. Use Type Safety ๐ช
Leverage TypeScriptโs type system to ensure all states implement required methods.
3. Document State Transitions ๐
Create clear documentation or diagrams showing how states transition.
4. Consider State History ๐
Sometimes you need to track previous states for undo/redo functionality.
5. Test State Transitions ๐งช
Write tests that verify all possible state transitions work correctly.
// ๐ Best practice example
abstract class BaseState implements PlayerState {
constructor(protected player: MusicPlayer) {}
// ๐ฏ Default implementations
play(): void {
this.logInvalidAction("play");
}
pause(): void {
this.logInvalidAction("pause");
}
stop(): void {
this.logInvalidAction("stop");
}
protected logInvalidAction(action: string): void {
console.log(`โ ๏ธ Cannot ${action} from ${this.constructor.name}`);
}
// ๐ Helper for transitions
protected transitionTo(StateClass: new (player: MusicPlayer) => PlayerState): void {
this.player.setState(new StateClass(this.player));
}
}
// ๐ต Now states are cleaner!
class BetterPlayingState extends BaseState {
pause(): void {
console.log("โธ๏ธ Pausing!");
this.transitionTo(PausedState);
}
stop(): void {
console.log("โน๏ธ Stopping!");
this.transitionTo(StoppedState);
}
}
๐งช Hands-On Exercise
Create a traffic light system with proper state management! ๐ฆ
Requirements:
- Three states: Red, Yellow, Green
- Proper transitions: Red โ Green โ Yellow โ Red
- Timer-based automatic transitions
- Manual override for emergencies
Your challenge:
// ๐ฏ Your task: Implement a traffic light system!
interface TrafficLightState {
// What methods should go here? ๐ค
}
class TrafficLight {
// Implement the context! ๐ช
}
// Implement the states! ๐ฆ
๐ก Click here for the solution!
// ๐ฆ Traffic light implementation
interface TrafficLightState {
enter(): void;
exit(): void;
next(): void;
emergency(): void;
}
class TrafficLight {
private state: TrafficLightState;
private timer?: NodeJS.Timeout;
constructor() {
this.state = new RedLight(this);
this.state.enter();
}
setState(state: TrafficLightState): void {
this.state.exit();
this.state = state;
this.state.enter();
}
setTimer(callback: () => void, duration: number): void {
this.clearTimer();
this.timer = setTimeout(callback, duration);
}
clearTimer(): void {
if (this.timer) {
clearTimeout(this.timer);
this.timer = undefined;
}
}
// ๐ฏ Public methods
next(): void {
this.state.next();
}
emergency(): void {
this.state.emergency();
}
}
// ๐ด Red light state
class RedLight implements TrafficLightState {
constructor(private light: TrafficLight) {}
enter(): void {
console.log("๐ด RED - Stop! Cars must wait");
this.light.setTimer(() => this.next(), 3000);
}
exit(): void {
console.log("Leaving red state...");
}
next(): void {
this.light.setState(new GreenLight(this.light));
}
emergency(): void {
console.log("๐จ Emergency! Staying RED for safety!");
this.light.clearTimer();
}
}
// ๐ข Green light state
class GreenLight implements TrafficLightState {
constructor(private light: TrafficLight) {}
enter(): void {
console.log("๐ข GREEN - Go! Cars can proceed");
this.light.setTimer(() => this.next(), 3000);
}
exit(): void {
console.log("Leaving green state...");
}
next(): void {
this.light.setState(new YellowLight(this.light));
}
emergency(): void {
console.log("๐จ Emergency! Switching to YELLOW!");
this.light.setState(new YellowLight(this.light));
}
}
// ๐ก Yellow light state
class YellowLight implements TrafficLightState {
constructor(private light: TrafficLight) {}
enter(): void {
console.log("๐ก YELLOW - Caution! Prepare to stop");
this.light.setTimer(() => this.next(), 1000);
}
exit(): void {
console.log("Leaving yellow state...");
}
next(): void {
this.light.setState(new RedLight(this.light));
}
emergency(): void {
console.log("๐จ Emergency! Going to RED!");
this.light.setState(new RedLight(this.light));
}
}
// ๐ฎ Test the traffic light!
const trafficLight = new TrafficLight();
// ๐ด RED - Stop! Cars must wait
// ... auto transitions happen with timers
// Manual control
trafficLight.next(); // Forces next state
trafficLight.emergency(); // Emergency override!
Great job! Youโve implemented a fully functional traffic light system! ๐
๐ Key Takeaways
Youโve mastered the State Pattern! Hereโs what you learned:
- ๐ฏ State Pattern encapsulates behavior based on object state
- ๐ States handle their own transitions keeping logic organized
- ๐ช TypeScript interfaces ensure all states implement required methods
- ๐จ Clean separation of concerns makes code maintainable
- ๐ Async states are possible for real-world applications
The State Pattern transforms complex conditional logic into elegant, maintainable code. No more massive switch statements or if-else chains! ๐
๐ค Next Steps
Congratulations on mastering the State Pattern! ๐ Youโve added a powerful tool to your TypeScript toolkit.
Ready for more patterns? Check out:
- ๐ Strategy Pattern - Choose algorithms at runtime
- ๐ Command Pattern - Encapsulate requests as objects
- ๐ Observer Pattern - Subscribe to state changes
Keep practicing with real projects โ maybe build a game character system or a workflow engine! The possibilities are endless! ๐ช
Remember: Great code isnโt just about making it work; itโs about making it elegant, maintainable, and fun to work with! Happy coding! ๐โจ