BehavioralTypeScriptverifiedVerified
Command Pattern in TypeScript
Encapsulates a request as an object, allowing you to parameterize clients, queue or log requests, and support undoable operations.
How to Implement the Command Pattern in TypeScript
1Step 1: Define the Command interface and receiver
interface Command {
execute(): void;
undo(): void;
}
class Counter {
value = 0;
}2Step 2: Implement concrete commands with undo support
class IncrementCommand implements Command {
constructor(private counter: Counter, private amount: number) {}
execute(): void {
this.counter.value += this.amount;
}
undo(): void {
this.counter.value -= this.amount;
}
}
class ResetCommand implements Command {
private previousValue = 0;
constructor(private counter: Counter) {}
execute(): void {
this.previousValue = this.counter.value;
this.counter.value = 0;
}
undo(): void {
this.counter.value = this.previousValue;
}
}3Step 3: Create a command history for execute and undo
class CommandHistory {
private history: Command[] = [];
execute(command: Command): void {
command.execute();
this.history.push(command);
}
undo(): void {
const command = this.history.pop();
command?.undo();
}
}4Step 4: Execute commands and undo them
const counter = new Counter();
const history = new CommandHistory();
history.execute(new IncrementCommand(counter, 10)); // value = 10
history.execute(new IncrementCommand(counter, 5)); // value = 15
history.execute(new ResetCommand(counter)); // value = 0
history.undo(); // value = 15
history.undo(); // value = 10// ── Text Editor with Command History ─────────────────────────────
interface EditorCommand {
readonly description: string;
execute(state: EditorState): EditorState;
undo(state: EditorState): EditorState;
}
interface EditorState {
content: string;
cursorPosition: number;
selectionStart: number;
selectionEnd: number;
}
// ── Commands ──────────────────────────────────────────────────────
class InsertTextCommand implements EditorCommand {
readonly description: string;
constructor(private text: string, private position: number) {
this.description = `Insert "${text}" at ${position}`;
}
execute(state: EditorState): EditorState {
const content =
state.content.slice(0, this.position) +
this.text +
state.content.slice(this.position);
return { ...state, content, cursorPosition: this.position + this.text.length };
}
undo(state: EditorState): EditorState {
const content =
state.content.slice(0, this.position) +
state.content.slice(this.position + this.text.length);
return { ...state, content, cursorPosition: this.position };
}
}
class DeleteTextCommand implements EditorCommand {
readonly description: string;
private deletedText = "";
constructor(private start: number, private end: number) {
this.description = `Delete range [${start}, ${end}]`;
}
execute(state: EditorState): EditorState {
this.deletedText = state.content.slice(this.start, this.end);
const content = state.content.slice(0, this.start) + state.content.slice(this.end);
return { ...state, content, cursorPosition: this.start };
}
undo(state: EditorState): EditorState {
const content =
state.content.slice(0, this.start) +
this.deletedText +
state.content.slice(this.start);
return { ...state, content, cursorPosition: this.end };
}
}
// ── Editor (Invoker) ──────────────────────────────────────────────
class TextEditor {
private state: EditorState = { content: "", cursorPosition: 0, selectionStart: 0, selectionEnd: 0 };
private undoStack: EditorCommand[] = [];
private redoStack: EditorCommand[] = [];
execute(command: EditorCommand): void {
this.state = command.execute(this.state);
this.undoStack.push(command);
this.redoStack = [];
}
undo(): string | null {
const command = this.undoStack.pop();
if (!command) return null;
this.state = command.undo(this.state);
this.redoStack.push(command);
return command.description;
}
redo(): string | null {
const command = this.redoStack.pop();
if (!command) return null;
this.state = command.execute(this.state);
this.undoStack.push(command);
return command.description;
}
getContent(): string { return this.state.content; }
getHistory(): string[] { return this.undoStack.map(c => c.description); }
}
export { TextEditor, InsertTextCommand, DeleteTextCommand, type EditorCommand, type EditorState };Command Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Command Pattern in the Real World
“Think of a restaurant order ticket. A waiter (invoker) takes your order and writes it on a slip (command). The slip is handed to the kitchen (receiver) which executes it. The waiter doesn't cook anything—they just carry and deliver orders. Tickets can be queued, cancelled before cooking, or reviewed in an audit log at day's end.”