+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 300 of 354

๐Ÿ“˜ Memento Pattern: State Snapshots

Master memento pattern: state snapshots in TypeScript with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

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 โœจ

๐ŸŽฏ Introduction

Welcome to this exciting tutorial on the Memento Pattern! ๐ŸŽ‰ In this guide, weโ€™ll explore how to capture and restore object states like a time machine for your code.

Youโ€™ll discover how the Memento Pattern can transform your TypeScript applications by providing undo/redo functionality, checkpoints, and state history. Whether youโ€™re building text editors ๐Ÿ“, games ๐ŸŽฎ, or complex applications ๐ŸŒ, understanding the Memento Pattern is essential for managing state changes gracefully.

By the end of this tutorial, youโ€™ll feel confident implementing state snapshots in your own projects! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Memento Pattern

๐Ÿค” What is the Memento Pattern?

The Memento Pattern is like taking a photograph ๐Ÿ“ธ of your applicationโ€™s state at a specific moment. Think of it as creating save points in a video game ๐ŸŽฎ that let you return to a previous state whenever you need.

In TypeScript terms, the Memento Pattern allows you to capture an objectโ€™s internal state without violating encapsulation, enabling you to restore the object to this state later. This means you can:

  • โœจ Implement undo/redo functionality
  • ๐Ÿš€ Create checkpoints in complex operations
  • ๐Ÿ›ก๏ธ Recover from errors by restoring previous states

๐Ÿ’ก Why Use the Memento Pattern?

Hereโ€™s why developers love the Memento Pattern:

  1. State History ๐Ÿ”’: Keep track of object state changes over time
  2. Encapsulation ๐Ÿ’ป: Preserve object boundaries while saving state
  3. Recovery Mechanism ๐Ÿ“–: Easily restore to previous states
  4. Debugging Power ๐Ÿ”ง: Trace through state changes to find issues

Real-world example: Imagine building a drawing app ๐ŸŽจ. With the Memento Pattern, users can undo their brush strokes and return to any previous version of their artwork!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Simple Example

Letโ€™s start with a friendly example:

// ๐Ÿ‘‹ Hello, Memento Pattern!
interface Memento {
  getState(): string;
}

// ๐ŸŽจ Creating a simple text editor
class TextEditor {
  private content: string = "";
  
  // โœ๏ธ Type some text
  type(text: string): void {
    this.content += text;
    console.log(`๐Ÿ“ Current content: "${this.content}"`);
  }
  
  // ๐Ÿ“ธ Save current state
  save(): Memento {
    return new TextMemento(this.content);
  }
  
  // ๐Ÿ”„ Restore from memento
  restore(memento: Memento): void {
    this.content = memento.getState();
    console.log(`โ†ฉ๏ธ Restored to: "${this.content}"`);
  }
}

// ๐Ÿ’พ Memento implementation
class TextMemento implements Memento {
  constructor(private state: string) {}
  
  getState(): string {
    return this.state;
  }
}

๐Ÿ’ก Explanation: Notice how the TextMemento stores the state privately and provides a clean interface to retrieve it. The TextEditor can save and restore its state without exposing its internals!

๐ŸŽฏ Common Patterns

Here are patterns youโ€™ll use daily:

// ๐Ÿ—๏ธ Pattern 1: Caretaker manages mementos
class History {
  private mementos: Memento[] = [];
  private currentIndex: number = -1;
  
  // ๐Ÿ“ฅ Add new state
  push(memento: Memento): void {
    this.currentIndex++;
    this.mementos = this.mementos.slice(0, this.currentIndex);
    this.mementos.push(memento);
    console.log(`๐Ÿ’พ Saved state #${this.currentIndex}`);
  }
  
  // โ†ฉ๏ธ Undo operation
  undo(): Memento | null {
    if (this.currentIndex > 0) {
      this.currentIndex--;
      return this.mementos[this.currentIndex];
    }
    return null;
  }
  
  // โ†ช๏ธ Redo operation
  redo(): Memento | null {
    if (this.currentIndex < this.mementos.length - 1) {
      this.currentIndex++;
      return this.mementos[this.currentIndex];
    }
    return null;
  }
}

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: Shopping Cart with History

Letโ€™s build something real:

// ๐Ÿ›๏ธ Product type
interface Product {
  id: string;
  name: string;
  price: number;
  emoji: string;
}

// ๐Ÿ’พ Cart state memento
interface CartMemento {
  getItems(): Product[];
  getTimestamp(): Date;
}

class CartSnapshot implements CartMemento {
  private items: Product[];
  private timestamp: Date;
  
  constructor(items: Product[]) {
    this.items = [...items]; // ๐ŸŽฏ Deep copy!
    this.timestamp = new Date();
  }
  
  getItems(): Product[] {
    return [...this.items]; // ๐Ÿ›ก๏ธ Return copy
  }
  
  getTimestamp(): Date {
    return this.timestamp;
  }
}

// ๐Ÿ›’ Shopping cart with history
class ShoppingCart {
  private items: Product[] = [];
  private history: CartSnapshot[] = [];
  
  // โž• Add item
  addItem(product: Product): void {
    this.saveSnapshot();
    this.items.push(product);
    console.log(`โœ… Added ${product.emoji} ${product.name}`);
  }
  
  // โž– Remove item
  removeItem(productId: string): void {
    this.saveSnapshot();
    const index = this.items.findIndex(item => item.id === productId);
    if (index > -1) {
      const removed = this.items.splice(index, 1)[0];
      console.log(`๐Ÿ—‘๏ธ Removed ${removed.emoji} ${removed.name}`);
    }
  }
  
  // ๐Ÿ“ธ Save current state
  private saveSnapshot(): void {
    this.history.push(new CartSnapshot(this.items));
  }
  
  // โ†ฉ๏ธ Undo last action
  undo(): void {
    if (this.history.length > 0) {
      const snapshot = this.history.pop()!;
      this.items = snapshot.getItems();
      console.log(`โ†ฉ๏ธ Restored cart to ${snapshot.getTimestamp().toLocaleTimeString()}`);
    }
  }
  
  // ๐Ÿ“‹ Show cart
  showCart(): void {
    console.log("๐Ÿ›’ Current Cart:");
    this.items.forEach(item => {
      console.log(`  ${item.emoji} ${item.name} - $${item.price}`);
    });
    console.log(`๐Ÿ’ฐ Total: $${this.getTotal()}`);
  }
  
  // ๐Ÿ’ฐ Calculate total
  private getTotal(): number {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }
}

// ๐ŸŽฎ Let's use it!
const cart = new ShoppingCart();
cart.addItem({ id: "1", name: "TypeScript Book", price: 29.99, emoji: "๐Ÿ“˜" });
cart.addItem({ id: "2", name: "Coffee", price: 4.99, emoji: "โ˜•" });
cart.showCart();
cart.removeItem("2");
cart.showCart();
cart.undo(); // Magic! โœจ
cart.showCart();

๐ŸŽฏ Try it yourself: Add a redo method and a maximum history limit!

๐ŸŽฎ Example 2: Game State Manager

Letโ€™s make it fun:

// ๐ŸŽฎ Game character state
interface GameState {
  health: number;
  mana: number;
  position: { x: number; y: number };
  inventory: string[];
  level: number;
}

// ๐Ÿ’พ Game memento with metadata
class GameSnapshot {
  private state: GameState;
  private saveTime: Date;
  private saveName: string;
  
  constructor(state: GameState, name: string) {
    this.state = JSON.parse(JSON.stringify(state)); // Deep clone
    this.saveTime = new Date();
    this.saveName = name;
  }
  
  getState(): GameState {
    return JSON.parse(JSON.stringify(this.state));
  }
  
  getInfo(): string {
    return `๐Ÿ’พ ${this.saveName} - ${this.saveTime.toLocaleString()}`;
  }
}

// ๐Ÿฐ Game with save system
class RPGGame {
  private state: GameState = {
    health: 100,
    mana: 50,
    position: { x: 0, y: 0 },
    inventory: ["๐Ÿ—ก๏ธ Sword", "๐Ÿ›ก๏ธ Shield"],
    level: 1
  };
  
  private saves: Map<string, GameSnapshot> = new Map();
  private autosaves: GameSnapshot[] = [];
  private maxAutosaves = 3;
  
  // ๐ŸŽฏ Player actions
  takeDamage(amount: number): void {
    this.autosave();
    this.state.health = Math.max(0, this.state.health - amount);
    console.log(`๐Ÿ’” Took ${amount} damage! Health: ${this.state.health}`);
    
    if (this.state.health === 0) {
      console.log("โ˜ ๏ธ Game Over! Use loadGame() to restore a save.");
    }
  }
  
  // ๐ŸŽฏ Move character
  move(x: number, y: number): void {
    this.state.position = { x, y };
    console.log(`๐Ÿšถ Moved to (${x}, ${y})`);
  }
  
  // ๐ŸŽ’ Add item to inventory
  collectItem(item: string): void {
    this.autosave();
    this.state.inventory.push(item);
    console.log(`โœจ Collected ${item}!`);
  }
  
  // ๐Ÿ“ˆ Level up
  levelUp(): void {
    this.state.level++;
    this.state.health = 100;
    this.state.mana += 20;
    console.log(`๐ŸŽ‰ Level ${this.state.level}! Full health restored!`);
  }
  
  // ๐Ÿ’พ Manual save
  saveGame(saveName: string): void {
    this.saves.set(saveName, new GameSnapshot(this.state, saveName));
    console.log(`๐Ÿ’พ Game saved as "${saveName}"`);
  }
  
  // ๐Ÿ“‚ Load save
  loadGame(saveName: string): void {
    const save = this.saves.get(saveName);
    if (save) {
      this.state = save.getState();
      console.log(`๐Ÿ“‚ Loaded save: ${save.getInfo()}`);
      this.showStatus();
    } else {
      console.log(`โŒ Save "${saveName}" not found!`);
    }
  }
  
  // ๐Ÿ”„ Autosave
  private autosave(): void {
    this.autosaves.push(new GameSnapshot(this.state, "Autosave"));
    if (this.autosaves.length > this.maxAutosaves) {
      this.autosaves.shift();
    }
  }
  
  // ๐Ÿ“Š Show game status
  showStatus(): void {
    console.log("๐ŸŽฎ Game Status:");
    console.log(`  โค๏ธ Health: ${this.state.health}/100`);
    console.log(`  ๐Ÿ’™ Mana: ${this.state.mana}`);
    console.log(`  ๐Ÿ“ Position: (${this.state.position.x}, ${this.state.position.y})`);
    console.log(`  ๐ŸŽ’ Inventory: ${this.state.inventory.join(", ")}`);
    console.log(`  โญ Level: ${this.state.level}`);
  }
  
  // ๐Ÿ“‹ List saves
  listSaves(): void {
    console.log("๐Ÿ’พ Available Saves:");
    this.saves.forEach((save, name) => {
      console.log(`  ${save.getInfo()}`);
    });
  }
}

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Advanced Topic 1: Typed Memento with Generics

When youโ€™re ready to level up, try this advanced pattern:

// ๐ŸŽฏ Generic memento interface
interface TypedMemento<T> {
  getState(): T;
  getMetadata(): MementoMetadata;
}

interface MementoMetadata {
  timestamp: Date;
  version: string;
  description?: string;
}

// ๐Ÿช„ Advanced memento implementation
class VersionedMemento<T> implements TypedMemento<T> {
  private state: T;
  private metadata: MementoMetadata;
  
  constructor(state: T, description?: string) {
    this.state = this.deepClone(state);
    this.metadata = {
      timestamp: new Date(),
      version: "1.0.0",
      description
    };
  }
  
  getState(): T {
    return this.deepClone(this.state);
  }
  
  getMetadata(): MementoMetadata {
    return { ...this.metadata };
  }
  
  private deepClone(obj: T): T {
    return JSON.parse(JSON.stringify(obj));
  }
}

// ๐Ÿš€ Advanced state manager
class StateManager<T> {
  private current: T;
  private history: TypedMemento<T>[] = [];
  private future: TypedMemento<T>[] = [];
  
  constructor(initialState: T) {
    this.current = initialState;
  }
  
  // ๐Ÿ“ธ Save with description
  checkpoint(description?: string): void {
    this.future = []; // Clear redo stack
    this.history.push(new VersionedMemento(this.current, description));
    console.log(`๐Ÿ“ธ Checkpoint created: ${description || "No description"}`);
  }
  
  // โ†ฉ๏ธ Undo with info
  undo(): boolean {
    const memento = this.history.pop();
    if (memento) {
      this.future.push(new VersionedMemento(this.current));
      this.current = memento.getState();
      console.log(`โ†ฉ๏ธ Restored: ${memento.getMetadata().description}`);
      return true;
    }
    return false;
  }
  
  // โ†ช๏ธ Redo with info
  redo(): boolean {
    const memento = this.future.pop();
    if (memento) {
      this.history.push(new VersionedMemento(this.current));
      this.current = memento.getState();
      console.log(`โ†ช๏ธ Redone: ${memento.getMetadata().description}`);
      return true;
    }
    return false;
  }
  
  // ๐ŸŽฏ Get current state
  getState(): T {
    return this.current;
  }
  
  // ๐Ÿ“Š History info
  getHistoryInfo(): void {
    console.log(`๐Ÿ“Š History: ${this.history.length} states`);
    console.log(`๐Ÿ“Š Future: ${this.future.length} states`);
  }
}

๐Ÿ—๏ธ Advanced Topic 2: Differential Memento

For the brave developers:

// ๐Ÿš€ Store only differences for efficiency
interface DiffMemento<T> {
  applyForward(state: T): T;
  applyBackward(state: T): T;
}

class DifferentialMemento<T extends object> implements DiffMemento<T> {
  private forwardDiff: Partial<T>;
  private backwardDiff: Partial<T>;
  
  constructor(oldState: T, newState: T) {
    this.forwardDiff = this.calculateDiff(oldState, newState);
    this.backwardDiff = this.calculateDiff(newState, oldState);
  }
  
  applyForward(state: T): T {
    return { ...state, ...this.forwardDiff };
  }
  
  applyBackward(state: T): T {
    return { ...state, ...this.backwardDiff };
  }
  
  private calculateDiff(from: T, to: T): Partial<T> {
    const diff: Partial<T> = {};
    for (const key in to) {
      if (from[key] !== to[key]) {
        diff[key] = to[key];
      }
    }
    return diff;
  }
}

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Shallow Copy Trap

// โŒ Wrong way - shallow copy shares references!
class BadMemento {
  constructor(private state: any[]) {}
  
  getState(): any[] {
    return this.state; // ๐Ÿ’ฅ Same reference!
  }
}

// โœ… Correct way - deep copy for safety!
class GoodMemento {
  private state: any[];
  
  constructor(state: any[]) {
    this.state = JSON.parse(JSON.stringify(state)); // ๐Ÿ›ก๏ธ Deep copy
  }
  
  getState(): any[] {
    return JSON.parse(JSON.stringify(this.state)); // โœ… Return copy
  }
}

๐Ÿคฏ Pitfall 2: Memory Leaks

// โŒ Dangerous - unlimited history!
class MemoryLeaker {
  private history: any[] = [];
  
  save(state: any): void {
    this.history.push(state); // ๐Ÿ’ฅ Grows forever!
  }
}

// โœ… Safe - limited history!
class MemoryFriendly {
  private history: any[] = [];
  private maxHistory = 50;
  
  save(state: any): void {
    this.history.push(state);
    if (this.history.length > this.maxHistory) {
      this.history.shift(); // โœ… Remove oldest
    }
  }
}

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ Encapsulation First: Never expose internal state directly
  2. ๐Ÿ“ Deep Copy Always: Prevent accidental state mutations
  3. ๐Ÿ›ก๏ธ Limit History Size: Avoid memory issues in long-running apps
  4. ๐ŸŽจ Metadata Matters: Store context with your mementos
  5. โœจ Immutability Rules: Treat saved states as read-only

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Drawing App with Undo/Redo

Create a type-safe drawing application:

๐Ÿ“‹ Requirements:

  • โœ… Support drawing shapes (circles, rectangles, lines)
  • ๐Ÿท๏ธ Each shape has position, size, and color
  • ๐Ÿ‘ค Unlimited undo/redo functionality
  • ๐Ÿ“… Save named snapshots
  • ๐ŸŽจ Each shape needs an emoji identifier!

๐Ÿš€ Bonus Points:

  • Add shape grouping
  • Implement selective undo
  • Create a timeline view

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
// ๐ŸŽฏ Our type-safe drawing system!
interface Shape {
  id: string;
  type: "circle" | "rectangle" | "line";
  position: { x: number; y: number };
  size: { width: number; height: number };
  color: string;
  emoji: string;
}

interface CanvasMemento {
  getShapes(): Shape[];
  getDescription(): string;
}

class CanvasSnapshot implements CanvasMemento {
  private shapes: Shape[];
  private description: string;
  
  constructor(shapes: Shape[], description: string) {
    this.shapes = JSON.parse(JSON.stringify(shapes));
    this.description = description;
  }
  
  getShapes(): Shape[] {
    return JSON.parse(JSON.stringify(this.shapes));
  }
  
  getDescription(): string {
    return this.description;
  }
}

class DrawingCanvas {
  private shapes: Shape[] = [];
  private history: CanvasSnapshot[] = [];
  private currentIndex: number = -1;
  
  // ๐ŸŽจ Add shape
  addShape(type: Shape["type"], x: number, y: number): void {
    this.saveState("Add " + type);
    
    const shape: Shape = {
      id: Date.now().toString(),
      type,
      position: { x, y },
      size: { width: 50, height: 50 },
      color: this.getRandomColor(),
      emoji: this.getShapeEmoji(type)
    };
    
    this.shapes.push(shape);
    console.log(`โœ… Added ${shape.emoji} ${type} at (${x}, ${y})`);
  }
  
  // ๐Ÿ—‘๏ธ Remove shape
  removeShape(id: string): void {
    const shape = this.shapes.find(s => s.id === id);
    if (shape) {
      this.saveState(`Remove ${shape.type}`);
      this.shapes = this.shapes.filter(s => s.id !== id);
      console.log(`๐Ÿ—‘๏ธ Removed ${shape.emoji} ${shape.type}`);
    }
  }
  
  // ๐Ÿ’พ Save current state
  private saveState(description: string): void {
    // Remove any states after current index
    this.history = this.history.slice(0, this.currentIndex + 1);
    
    // Add new state
    this.history.push(new CanvasSnapshot(this.shapes, description));
    this.currentIndex++;
    
    console.log(`๐Ÿ’พ Saved: ${description}`);
  }
  
  // โ†ฉ๏ธ Undo
  undo(): void {
    if (this.currentIndex > 0) {
      this.currentIndex--;
      const snapshot = this.history[this.currentIndex];
      this.shapes = snapshot.getShapes();
      console.log(`โ†ฉ๏ธ Undo: ${this.history[this.currentIndex + 1].getDescription()}`);
    } else {
      console.log("โš ๏ธ Nothing to undo!");
    }
  }
  
  // โ†ช๏ธ Redo
  redo(): void {
    if (this.currentIndex < this.history.length - 1) {
      this.currentIndex++;
      const snapshot = this.history[this.currentIndex];
      this.shapes = snapshot.getShapes();
      console.log(`โ†ช๏ธ Redo: ${snapshot.getDescription()}`);
    } else {
      console.log("โš ๏ธ Nothing to redo!");
    }
  }
  
  // ๐Ÿ“Š Show canvas
  showCanvas(): void {
    console.log("๐ŸŽจ Canvas:");
    this.shapes.forEach(shape => {
      console.log(`  ${shape.emoji} ${shape.type} at (${shape.position.x}, ${shape.position.y})`);
    });
    console.log(`๐Ÿ“Š Total shapes: ${this.shapes.length}`);
    console.log(`๐Ÿ“š History: ${this.currentIndex + 1}/${this.history.length}`);
  }
  
  // ๐ŸŽฒ Helper methods
  private getRandomColor(): string {
    const colors = ["red", "blue", "green", "yellow", "purple"];
    return colors[Math.floor(Math.random() * colors.length)];
  }
  
  private getShapeEmoji(type: Shape["type"]): string {
    const emojis = { circle: "โญ•", rectangle: "๐ŸŸฆ", line: "๐Ÿ“" };
    return emojis[type];
  }
}

// ๐ŸŽฎ Test it out!
const canvas = new DrawingCanvas();
canvas.addShape("circle", 10, 10);
canvas.addShape("rectangle", 50, 50);
canvas.addShape("line", 100, 100);
canvas.showCanvas();
canvas.undo();
canvas.showCanvas();
canvas.redo();
canvas.showCanvas();

๐ŸŽ“ Key Takeaways

Youโ€™ve learned so much! Hereโ€™s what you can now do:

  • โœ… Create state snapshots with confidence ๐Ÿ’ช
  • โœ… Implement undo/redo functionality like a pro ๐Ÿ›ก๏ธ
  • โœ… Manage state history efficiently ๐ŸŽฏ
  • โœ… Avoid common memory issues ๐Ÿ›
  • โœ… Build time-travel features with TypeScript! ๐Ÿš€

Remember: The Memento Pattern is your time machine for state management. Use it wisely! ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered the Memento Pattern!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Practice with the drawing app exercise
  2. ๐Ÿ—๏ธ Add undo/redo to your current project
  3. ๐Ÿ“š Move on to our next tutorial: Observer Pattern
  4. ๐ŸŒŸ Share your time-traveling creations!

Remember: Every great app deserves a good undo button. Keep coding, keep learning, and most importantly, have fun! ๐Ÿš€


Happy coding! ๐ŸŽ‰๐Ÿš€โœจ