BehavioralTypeScriptverifiedVerified
Memento Pattern in TypeScript
Captures and externalizes an object's internal state without violating encapsulation, allowing the object to be restored to that state later.
How to Implement the Memento Pattern in TypeScript
1Step 1: Define the Memento that captures editor state
class EditorMemento {
constructor(
private readonly content: string,
private readonly cursor: number
) {}
getContent(): string { return this.content; }
getCursor(): number { return this.cursor; }
}2Step 2: Implement the Editor with save and restore
class Editor {
private content = "";
private cursor = 0;
type(text: string): void {
this.content =
this.content.slice(0, this.cursor) +
text +
this.content.slice(this.cursor);
this.cursor += text.length;
}
save(): EditorMemento {
return new EditorMemento(this.content, this.cursor);
}
restore(memento: EditorMemento): void {
this.content = memento.getContent();
this.cursor = memento.getCursor();
}
toString(): string {
return `"${this.content}" (cursor: ${this.cursor})`;
}
}3Step 3: Create a History caretaker to manage snapshots
class History {
private mementos: EditorMemento[] = [];
push(memento: EditorMemento): void {
this.mementos.push(memento);
}
pop(): EditorMemento | undefined {
return this.mementos.pop();
}
}4Step 4: Take snapshots and undo changes
const editor = new Editor();
const history = new History();
history.push(editor.save());
editor.type("Hello");
history.push(editor.save());
editor.type(", World");
console.log(editor.toString()); // "Hello, World" (cursor: 12)
editor.restore(history.pop()!);
console.log(editor.toString()); // "Hello" (cursor: 5)
editor.restore(history.pop()!);
console.log(editor.toString()); // "" (cursor: 0)// ── Configuration Snapshots with Metadata ─────────────────────────
interface ConfigSnapshot<T> {
readonly id: string;
readonly label: string;
readonly state: Readonly<T>;
readonly createdAt: number;
readonly tags: string[];
}
class ConfigMemento<T> {
readonly id: string;
readonly createdAt: number;
constructor(
private readonly state: T,
public readonly label: string,
public readonly tags: string[] = []
) {
this.id = crypto.randomUUID();
this.createdAt = Date.now();
}
getSnapshot(): ConfigSnapshot<T> {
return {
id: this.id,
label: this.label,
state: Object.freeze(structuredClone(this.state)),
createdAt: this.createdAt,
tags: [...this.tags],
};
}
}
// ── Caretaker with Search ─────────────────────────────────────────
class SnapshotHistory<T> {
private snapshots: ConfigMemento<T>[] = [];
private readonly maxSize: number;
constructor(maxSize = 50) {
this.maxSize = maxSize;
}
save(memento: ConfigMemento<T>): void {
this.snapshots.push(memento);
if (this.snapshots.length > this.maxSize) {
this.snapshots.shift();
}
}
getById(id: string): ConfigMemento<T> | undefined {
return this.snapshots.find(s => s.id === id);
}
getByTag(tag: string): ConfigMemento<T>[] {
return this.snapshots.filter(s => s.tags.includes(tag));
}
latest(): ConfigMemento<T> | undefined {
return this.snapshots[this.snapshots.length - 1];
}
list(): ConfigSnapshot<T>[] {
return this.snapshots.map(s => s.getSnapshot());
}
}
// ── Configurable (Originator) ─────────────────────────────────────
interface AppConfig {
theme: "light" | "dark";
language: string;
features: Record<string, boolean>;
rateLimits: { requestsPerMinute: number; burstSize: number };
}
class AppConfigManager {
private config: AppConfig;
private history = new SnapshotHistory<AppConfig>();
constructor(initial: AppConfig) {
this.config = structuredClone(initial);
}
update(patch: Partial<AppConfig>, label: string, tags: string[] = []): void {
const memento = new ConfigMemento(this.config, label, tags);
this.history.save(memento);
this.config = { ...this.config, ...patch };
}
rollback(snapshotId?: string): ConfigSnapshot<AppConfig> | null {
const memento = snapshotId
? this.history.getById(snapshotId)
: this.history.latest();
if (!memento) return null;
const snapshot = memento.getSnapshot();
this.config = structuredClone(snapshot.state as AppConfig);
return snapshot;
}
getConfig(): Readonly<AppConfig> { return this.config; }
getHistory(): ConfigSnapshot<AppConfig>[] { return this.history.list(); }
getSnapshotsByTag(tag: string): ConfigSnapshot<AppConfig>[] {
return this.history.getByTag(tag).map(m => m.getSnapshot());
}
}
export { AppConfigManager, ConfigMemento, SnapshotHistory, type AppConfig, type ConfigSnapshot };Memento Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Memento Pattern in the Real World
“Think of a video game save point. When you save, the game (originator) packages your character's stats, inventory, and position into a save file (memento). The save system (caretaker) stores these files without understanding their contents. When you die and reload, the save file is handed back to the game, which restores everything exactly as it was.”