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:
- State History ๐: Keep track of object state changes over time
- Encapsulation ๐ป: Preserve object boundaries while saving state
- Recovery Mechanism ๐: Easily restore to previous states
- 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
- ๐ฏ Encapsulation First: Never expose internal state directly
- ๐ Deep Copy Always: Prevent accidental state mutations
- ๐ก๏ธ Limit History Size: Avoid memory issues in long-running apps
- ๐จ Metadata Matters: Store context with your mementos
- โจ 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:
- ๐ป Practice with the drawing app exercise
- ๐๏ธ Add undo/redo to your current project
- ๐ Move on to our next tutorial: Observer Pattern
- ๐ 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! ๐๐โจ