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
Have you ever wished you could treat actions like objects? ๐ค Imagine being able to store, queue, undo, and redo operations just like you handle regular data! Thatโs exactly what the Command Pattern brings to the table! ๐
The Command Pattern is like having a remote control ๐ฎ where each button is a command that can be executed, undone, or even saved for later. Itโs one of the most powerful behavioral patterns in TypeScript, and today weโre going to make it your new superpower! ๐ช
๐ Understanding Command Pattern
Think of the Command Pattern like ordering at a restaurant ๐ฝ๏ธ. When you tell the waiter what you want, they write it down on a piece of paper (the command). This order can be:
- Passed to the kitchen (executed) ๐จโ๐ณ
- Modified or cancelled (undone) โ
- Tracked for billing (logged) ๐
- Queued during busy times (delayed execution) โฐ
In programming terms, the Command Pattern encapsulates a request as an object, allowing you to:
- Parameterize objects with different requests ๐ฆ
- Queue or log requests ๐
- Support undo operations โฉ๏ธ
- Build macro commands from smaller ones ๐๏ธ
๐ง Basic Syntax and Usage
Letโs start with a simple command pattern implementation:
// ๐ฏ The Command interface
interface Command {
execute(): void;
undo(): void;
}
// ๐ก Receiver - the object that performs the actual work
class Light {
private isOn = false;
turnOn(): void {
this.isOn = true;
console.log("Light is ON! ๐ก");
}
turnOff(): void {
this.isOn = false;
console.log("Light is OFF! ๐");
}
getStatus(): string {
return this.isOn ? "ON ๐ก" : "OFF ๐";
}
}
// ๐ฎ Concrete Command
class TurnOnLightCommand implements Command {
constructor(private light: Light) {}
execute(): void {
this.light.turnOn();
}
undo(): void {
this.light.turnOff();
}
}
// ๐ฎ Another Concrete Command
class TurnOffLightCommand implements Command {
constructor(private light: Light) {}
execute(): void {
this.light.turnOff();
}
undo(): void {
this.light.turnOn();
}
}
// ๐๏ธ Invoker - the remote control
class RemoteControl {
private currentCommand: Command | null = null;
setCommand(command: Command): void {
this.currentCommand = command;
}
pressButton(): void {
if (this.currentCommand) {
this.currentCommand.execute();
}
}
pressUndo(): void {
if (this.currentCommand) {
this.currentCommand.undo();
}
}
}
// ๐ Let's use it!
const livingRoomLight = new Light();
const remote = new RemoteControl();
const turnOn = new TurnOnLightCommand(livingRoomLight);
const turnOff = new TurnOffLightCommand(livingRoomLight);
remote.setCommand(turnOn);
remote.pressButton(); // Light is ON! ๐ก
remote.pressUndo(); // Light is OFF! ๐
๐ก Practical Examples
Example 1: Text Editor with Undo/Redo ๐
Letโs build a simple text editor that supports undo and redo operations:
// ๐ The document we're editing
class TextDocument {
private content = "";
write(text: string): void {
this.content += text;
}
delete(length: number): void {
this.content = this.content.substring(0, this.content.length - length);
}
getContent(): string {
return this.content;
}
setContent(content: string): void {
this.content = content;
}
}
// ๐ฏ Abstract command with state saving
abstract class TextCommand implements Command {
protected previousState = "";
constructor(protected document: TextDocument) {}
abstract execute(): void;
abstract undo(): void;
}
// โ๏ธ Write command
class WriteCommand extends TextCommand {
constructor(document: TextDocument, private text: string) {
super(document);
}
execute(): void {
this.previousState = this.document.getContent();
this.document.write(this.text);
console.log(`Wrote: "${this.text}" ๐`);
}
undo(): void {
this.document.setContent(this.previousState);
console.log(`Undid write of: "${this.text}" โฉ๏ธ`);
}
}
// โ๏ธ Delete command
class DeleteCommand extends TextCommand {
private deletedText = "";
constructor(document: TextDocument, private length: number) {
super(document);
}
execute(): void {
const content = this.document.getContent();
this.deletedText = content.substring(content.length - this.length);
this.document.delete(this.length);
console.log(`Deleted: "${this.deletedText}" ๐๏ธ`);
}
undo(): void {
this.document.write(this.deletedText);
console.log(`Restored: "${this.deletedText}" ๐`);
}
}
// ๐ Command History Manager
class CommandHistory {
private history: Command[] = [];
private currentIndex = -1;
execute(command: Command): void {
// Remove any commands after current index
this.history = this.history.slice(0, this.currentIndex + 1);
// Add new command
command.execute();
this.history.push(command);
this.currentIndex++;
}
undo(): void {
if (this.currentIndex >= 0) {
const command = this.history[this.currentIndex];
command.undo();
this.currentIndex--;
console.log("Undo successful! โฉ๏ธ");
} else {
console.log("Nothing to undo! ๐คท");
}
}
redo(): void {
if (this.currentIndex < this.history.length - 1) {
this.currentIndex++;
const command = this.history[this.currentIndex];
command.execute();
console.log("Redo successful! โช๏ธ");
} else {
console.log("Nothing to redo! ๐คท");
}
}
}
// ๐ฎ Let's test our text editor!
const doc = new TextDocument();
const history = new CommandHistory();
history.execute(new WriteCommand(doc, "Hello ")); // Wrote: "Hello " ๐
history.execute(new WriteCommand(doc, "World!")); // Wrote: "World!" ๐
console.log(`Document: "${doc.getContent()}"`); // Document: "Hello World!"
history.undo(); // Undid write of: "World!" โฉ๏ธ
console.log(`Document: "${doc.getContent()}"`); // Document: "Hello "
history.redo(); // Wrote: "World!" ๐
console.log(`Document: "${doc.getContent()}"`); // Document: "Hello World!"
history.execute(new DeleteCommand(doc, 6)); // Deleted: "World!" ๐๏ธ
console.log(`Document: "${doc.getContent()}"`); // Document: "Hello "
Example 2: Smart Home Automation ๐
Letโs create a smart home system with macro commands:
// ๐ Various home devices
class Television {
turnOn(): void { console.log("TV is ON ๐บ"); }
turnOff(): void { console.log("TV is OFF ๐บ"); }
setChannel(channel: number): void { console.log(`Channel set to ${channel} ๐บ`); }
}
class AirConditioner {
turnOn(): void { console.log("AC is ON โ๏ธ"); }
turnOff(): void { console.log("AC is OFF โ๏ธ"); }
setTemperature(temp: number): void { console.log(`Temperature set to ${temp}ยฐC ๐ก๏ธ`); }
}
class SecuritySystem {
arm(): void { console.log("Security ARMED ๐"); }
disarm(): void { console.log("Security DISARMED ๐"); }
}
// ๐ฏ Device commands
class TVOnCommand implements Command {
constructor(private tv: Television, private channel: number = 1) {}
execute(): void {
this.tv.turnOn();
this.tv.setChannel(this.channel);
}
undo(): void {
this.tv.turnOff();
}
}
class ACOnCommand implements Command {
constructor(private ac: AirConditioner, private temperature: number = 22) {}
execute(): void {
this.ac.turnOn();
this.ac.setTemperature(this.temperature);
}
undo(): void {
this.ac.turnOff();
}
}
class SecurityArmCommand implements Command {
constructor(private security: SecuritySystem) {}
execute(): void {
this.security.arm();
}
undo(): void {
this.security.disarm();
}
}
// ๐ญ Macro Command - executes multiple commands
class MacroCommand implements Command {
constructor(private commands: Command[]) {}
execute(): void {
console.log("๐ฌ Executing macro command...");
this.commands.forEach(command => command.execute());
}
undo(): void {
console.log("โฉ๏ธ Undoing macro command...");
// Undo in reverse order!
[...this.commands].reverse().forEach(command => command.undo());
}
}
// ๐ Smart Home Controller
class SmartHomeController {
private commands: Map<string, Command> = new Map();
setCommand(name: string, command: Command): void {
this.commands.set(name, command);
console.log(`Command "${name}" programmed! ๐ฎ`);
}
executeCommand(name: string): void {
const command = this.commands.get(name);
if (command) {
command.execute();
} else {
console.log(`Command "${name}" not found! ๐`);
}
}
undoCommand(name: string): void {
const command = this.commands.get(name);
if (command) {
command.undo();
}
}
}
// ๐ฎ Let's automate our home!
const tv = new Television();
const ac = new AirConditioner();
const security = new SecuritySystem();
const controller = new SmartHomeController();
// Individual commands
controller.setCommand("watchTV", new TVOnCommand(tv, 5));
controller.setCommand("coolRoom", new ACOnCommand(ac, 20));
// "Leaving Home" macro
const leavingHome = new MacroCommand([
new TVOnCommand(tv, 1), // Turn on TV to news channel
new ACOnCommand(ac, 25), // Set AC to eco mode
new SecurityArmCommand(security) // Arm security
]);
controller.setCommand("leaving", leavingHome);
// Execute the macro!
controller.executeCommand("leaving");
// ๐ฌ Executing macro command...
// TV is ON ๐บ
// Channel set to 1 ๐บ
// AC is ON โ๏ธ
// Temperature set to 25ยฐC ๐ก๏ธ
// Security ARMED ๐
// Oops, forgot something!
controller.undoCommand("leaving");
// โฉ๏ธ Undoing macro command...
// Security DISARMED ๐
// AC is OFF โ๏ธ
// TV is OFF ๐บ
Example 3: Game Action System ๐ฎ
Letโs create a game where player actions can be queued and replayed:
// ๐ฎ Game character
class GameCharacter {
private x = 0;
private y = 0;
private health = 100;
private inventory: string[] = [];
move(dx: number, dy: number): void {
this.x += dx;
this.y += dy;
console.log(`Moved to (${this.x}, ${this.y}) ๐`);
}
takeDamage(amount: number): void {
this.health -= amount;
console.log(`Took ${amount} damage! Health: ${this.health} ๐`);
}
heal(amount: number): void {
this.health += amount;
console.log(`Healed ${amount}! Health: ${this.health} ๐`);
}
collectItem(item: string): void {
this.inventory.push(item);
console.log(`Collected ${item}! ๐`);
}
dropItem(item: string): void {
const index = this.inventory.indexOf(item);
if (index > -1) {
this.inventory.splice(index, 1);
console.log(`Dropped ${item}! ๐๏ธ`);
}
}
getPosition(): { x: number; y: number } {
return { x: this.x, y: this.y };
}
}
// ๐ฏ Game commands
class MoveCommand implements Command {
private previousX = 0;
private previousY = 0;
constructor(
private character: GameCharacter,
private dx: number,
private dy: number
) {}
execute(): void {
const pos = this.character.getPosition();
this.previousX = pos.x;
this.previousY = pos.y;
this.character.move(this.dx, this.dy);
}
undo(): void {
const currentPos = this.character.getPosition();
const dx = this.previousX - currentPos.x;
const dy = this.previousY - currentPos.y;
this.character.move(dx, dy);
}
}
class CollectItemCommand implements Command {
constructor(
private character: GameCharacter,
private item: string
) {}
execute(): void {
this.character.collectItem(this.item);
}
undo(): void {
this.character.dropItem(this.item);
}
}
// ๐ฌ Action Replay System
class ActionReplay {
private actions: Command[] = [];
private isRecording = false;
startRecording(): void {
this.isRecording = true;
this.actions = [];
console.log("๐ด Recording started!");
}
stopRecording(): void {
this.isRecording = false;
console.log("โน๏ธ Recording stopped!");
}
recordAction(command: Command): void {
if (this.isRecording) {
this.actions.push(command);
}
command.execute();
}
replay(): void {
console.log("โถ๏ธ Replaying actions...");
this.actions.forEach((action, index) => {
setTimeout(() => {
console.log(` Action ${index + 1}:`);
action.execute();
}, index * 1000); // Delay each action by 1 second
});
}
}
// ๐ฎ Let's play!
const player = new GameCharacter();
const replay = new ActionReplay();
// Start recording
replay.startRecording();
// Record some actions
replay.recordAction(new MoveCommand(player, 5, 0)); // Moved to (5, 0) ๐
replay.recordAction(new CollectItemCommand(player, "๐ก๏ธ Sword")); // Collected ๐ก๏ธ Sword! ๐
replay.recordAction(new MoveCommand(player, 0, 3)); // Moved to (5, 3) ๐
replay.recordAction(new CollectItemCommand(player, "๐ก๏ธ Shield")); // Collected ๐ก๏ธ Shield! ๐
replay.stopRecording();
// Now replay the adventure!
console.log("\n๐ฌ Time to replay the adventure!");
replay.replay();
๐ Advanced Concepts
Command Queue with Priority ๐
// ๐ฏ Enhanced command with priority
interface PriorityCommand extends Command {
priority: number;
description: string;
}
class CommandQueue {
private queue: PriorityCommand[] = [];
private processing = false;
add(command: PriorityCommand): void {
this.queue.push(command);
// Sort by priority (higher number = higher priority)
this.queue.sort((a, b) => b.priority - a.priority);
console.log(`๐ฅ Queued: ${command.description} (Priority: ${command.priority})`);
}
async processQueue(): Promise<void> {
if (this.processing || this.queue.length === 0) return;
this.processing = true;
console.log("๐ Processing command queue...");
while (this.queue.length > 0) {
const command = this.queue.shift()!;
console.log(`โถ๏ธ Executing: ${command.description}`);
command.execute();
// Simulate async processing
await new Promise(resolve => setTimeout(resolve, 500));
}
this.processing = false;
console.log("โ
Queue processed!");
}
}
// ๐ฎ Example usage
class DatabaseCommand implements PriorityCommand {
constructor(
public priority: number,
public description: string,
private action: () => void
) {}
execute(): void {
this.action();
}
undo(): void {
console.log(`โฉ๏ธ Undoing: ${this.description}`);
}
}
const queue = new CommandQueue();
queue.add(new DatabaseCommand(1, "Save user profile", () => {
console.log("๐พ Saving user profile...");
}));
queue.add(new DatabaseCommand(3, "Process payment", () => {
console.log("๐ณ Processing payment...");
}));
queue.add(new DatabaseCommand(2, "Send email", () => {
console.log("๐ง Sending email...");
}));
// Process in priority order
queue.processQueue();
// Output:
// ๐ Processing command queue...
// โถ๏ธ Executing: Process payment
// ๐ณ Processing payment...
// โถ๏ธ Executing: Send email
// ๐ง Sending email...
// โถ๏ธ Executing: Save user profile
// ๐พ Saving user profile...
// โ
Queue processed!
Command with Validation and Rollback ๐
// ๐ฏ Command with validation
interface ValidatedCommand extends Command {
validate(): boolean;
rollback(): void;
}
class TransactionCommand implements ValidatedCommand {
private executed = false;
constructor(
private from: string,
private to: string,
private amount: number
) {}
validate(): boolean {
// Check if transaction is valid
if (this.amount <= 0) {
console.log("โ Invalid amount!");
return false;
}
if (this.from === this.to) {
console.log("โ Cannot transfer to same account!");
return false;
}
console.log("โ
Transaction validated!");
return true;
}
execute(): void {
if (!this.validate()) {
throw new Error("Transaction validation failed!");
}
console.log(`๐ธ Transferring $${this.amount} from ${this.from} to ${this.to}`);
this.executed = true;
}
undo(): void {
if (this.executed) {
console.log(`โฉ๏ธ Reversing transaction: $${this.amount} from ${this.to} to ${this.from}`);
this.executed = false;
}
}
rollback(): void {
console.log("๐ Rolling back transaction...");
this.undo();
}
}
// ๐ฆ Transaction processor
class TransactionProcessor {
private completedTransactions: ValidatedCommand[] = [];
async processTransaction(transaction: ValidatedCommand): Promise<void> {
try {
console.log("๐ Processing transaction...");
if (!transaction.validate()) {
throw new Error("Validation failed!");
}
transaction.execute();
this.completedTransactions.push(transaction);
console.log("โ
Transaction completed!");
} catch (error) {
console.log("โ Transaction failed!");
transaction.rollback();
throw error;
}
}
rollbackAll(): void {
console.log("๐จ Rolling back all transactions!");
[...this.completedTransactions].reverse().forEach(t => t.rollback());
this.completedTransactions = [];
}
}
โ ๏ธ Common Pitfalls and Solutions
โ Wrong: Commands with tight coupling
// โ Bad - Command knows too much about the receiver
class BadCommand implements Command {
execute(): void {
// Directly accessing internal implementation
document.getElementById('button')!.style.color = 'red';
window.localStorage.setItem('color', 'red');
fetch('/api/save-color', { method: 'POST', body: 'red' });
}
undo(): void {
// How do we undo all of this? ๐ฑ
}
}
โ Correct: Loosely coupled commands
// โ
Good - Command delegates to receiver
class ColorChangeCommand implements Command {
private previousColor: string = '';
constructor(
private colorService: ColorService,
private newColor: string
) {}
execute(): void {
this.previousColor = this.colorService.getCurrentColor();
this.colorService.setColor(this.newColor);
}
undo(): void {
this.colorService.setColor(this.previousColor);
}
}
// The service handles all the details
class ColorService {
getCurrentColor(): string {
return this.currentColor;
}
setColor(color: string): void {
this.updateUI(color);
this.saveToStorage(color);
this.syncToServer(color);
this.currentColor = color;
}
private currentColor = 'white';
private updateUI(color: string): void { /* ... */ }
private saveToStorage(color: string): void { /* ... */ }
private syncToServer(color: string): void { /* ... */ }
}
โ Wrong: Forgetting to save state for undo
// โ Bad - Can't undo properly
class BadDeleteCommand implements Command {
constructor(private list: string[], private index: number) {}
execute(): void {
this.list.splice(this.index, 1); // Lost the deleted item! ๐ฑ
}
undo(): void {
// What do we restore? ๐คท
}
}
โ Correct: Saving state for undo
// โ
Good - Saves state for undo
class GoodDeleteCommand implements Command {
private deletedItem: string | undefined;
constructor(private list: string[], private index: number) {}
execute(): void {
if (this.index >= 0 && this.index < this.list.length) {
this.deletedItem = this.list[this.index];
this.list.splice(this.index, 1);
}
}
undo(): void {
if (this.deletedItem !== undefined) {
this.list.splice(this.index, 0, this.deletedItem);
}
}
}
๐ ๏ธ Best Practices
-
Keep Commands Simple and Focused ๐ฏ
- Each command should do one thing well
- Avoid complex logic in commands
- Delegate actual work to receivers
-
Always Implement Undo Properly โฉ๏ธ
- Save necessary state before execution
- Test undo functionality thoroughly
- Consider edge cases (what if undo is called twice?)
-
Use Command Queues for Async Operations ๐
- Queue commands for batch processing
- Add priority support when needed
- Handle failures gracefully
-
Consider Macro Commands for Complex Operations ๐ญ
- Group related commands together
- Ensure proper undo order (usually reverse)
- Test macro commands thoroughly
-
Document Command Side Effects ๐
- Clearly state what each command does
- Document any external dependencies
- Explain undo behavior
๐งช Hands-On Exercise
Create a drawing application that supports undo/redo for different shape operations! ๐จ
Your challenge: Implement commands for drawing shapes with full undo/redo support.
// ๐ฏ Your task: Complete this drawing application!
interface Shape {
id: string;
type: 'circle' | 'rectangle' | 'line';
x: number;
y: number;
color: string;
}
class DrawingCanvas {
private shapes: Shape[] = [];
private nextId = 1;
addShape(type: Shape['type'], x: number, y: number, color: string): string {
const id = `shape-${this.nextId++}`;
const shape: Shape = { id, type, x, y, color };
this.shapes.push(shape);
console.log(`Drew ${color} ${type} at (${x}, ${y}) ๐จ`);
return id;
}
removeShape(id: string): Shape | undefined {
const index = this.shapes.findIndex(s => s.id === id);
if (index > -1) {
const [removed] = this.shapes.splice(index, 1);
console.log(`Removed ${removed.color} ${removed.type} ๐๏ธ`);
return removed;
}
return undefined;
}
moveShape(id: string, newX: number, newY: number): { oldX: number; oldY: number } | undefined {
const shape = this.shapes.find(s => s.id === id);
if (shape) {
const oldPos = { oldX: shape.x, oldY: shape.y };
shape.x = newX;
shape.y = newY;
console.log(`Moved ${shape.type} to (${newX}, ${newY}) ๐`);
return oldPos;
}
return undefined;
}
getShapes(): Shape[] {
return [...this.shapes];
}
}
// TODO: Implement these commands!
class DrawShapeCommand implements Command {
// Your code here! ๐ฏ
execute(): void {
// Implement drawing
}
undo(): void {
// Implement undo
}
}
class MoveShapeCommand implements Command {
// Your code here! ๐ฏ
execute(): void {
// Implement moving
}
undo(): void {
// Implement undo
}
}
class DeleteShapeCommand implements Command {
// Your code here! ๐ฏ
execute(): void {
// Implement deletion
}
undo(): void {
// Implement undo
}
}
// Test your implementation!
const canvas = new DrawingCanvas();
const history = new CommandHistory();
// Try drawing shapes and undoing/redoing! ๐จ
๐ก Click here for the solution!
// ๐ฏ Solution: Complete drawing application with commands!
class DrawShapeCommand implements Command {
private shapeId: string | null = null;
constructor(
private canvas: DrawingCanvas,
private type: Shape['type'],
private x: number,
private y: number,
private color: string
) {}
execute(): void {
this.shapeId = this.canvas.addShape(this.type, this.x, this.y, this.color);
}
undo(): void {
if (this.shapeId) {
this.canvas.removeShape(this.shapeId);
}
}
}
class MoveShapeCommand implements Command {
private oldPosition: { oldX: number; oldY: number } | null = null;
constructor(
private canvas: DrawingCanvas,
private shapeId: string,
private newX: number,
private newY: number
) {}
execute(): void {
this.oldPosition = this.canvas.moveShape(this.shapeId, this.newX, this.newY) || null;
}
undo(): void {
if (this.oldPosition) {
this.canvas.moveShape(this.shapeId, this.oldPosition.oldX, this.oldPosition.oldY);
}
}
}
class DeleteShapeCommand implements Command {
private deletedShape: Shape | null = null;
constructor(
private canvas: DrawingCanvas,
private shapeId: string
) {}
execute(): void {
this.deletedShape = this.canvas.removeShape(this.shapeId) || null;
}
undo(): void {
if (this.deletedShape) {
this.canvas.addShape(
this.deletedShape.type,
this.deletedShape.x,
this.deletedShape.y,
this.deletedShape.color
);
}
}
}
// ๐จ Let's test our drawing app!
const canvas = new DrawingCanvas();
const history = new CommandHistory();
// Draw some shapes
history.execute(new DrawShapeCommand(canvas, 'circle', 50, 50, 'red'));
history.execute(new DrawShapeCommand(canvas, 'rectangle', 100, 100, 'blue'));
history.execute(new DrawShapeCommand(canvas, 'line', 150, 150, 'green'));
console.log('\n๐ Current shapes:', canvas.getShapes().length);
// Move a shape
const shapes = canvas.getShapes();
if (shapes[0]) {
history.execute(new MoveShapeCommand(canvas, shapes[0].id, 75, 75));
}
// Undo the move
history.undo(); // โฉ๏ธ Shape back to original position!
// Delete a shape
if (shapes[1]) {
history.execute(new DeleteShapeCommand(canvas, shapes[1].id));
}
// Undo the delete
history.undo(); // โฉ๏ธ Shape restored!
console.log('\n๐ Final shapes:', canvas.getShapes().length);
๐ Key Takeaways
Youโve mastered the Command Pattern! Hereโs what youโve learned:
- Encapsulation Power ๐ฆ: Commands wrap actions as objects
- Undo/Redo Magic โฉ๏ธ: Full control over action history
- Queuing Excellence ๐: Process commands when ready
- Macro Mastery ๐ญ: Combine simple commands into complex ones
- Decoupling Benefits ๐: Separate what from how
The Command Pattern is your Swiss Army knife for handling operations in TypeScript! Whether youโre building text editors, game engines, or smart home systems, this pattern gives you incredible flexibility and control. ๐
๐ค Next Steps
Congratulations on mastering the Command Pattern! ๐ Your TypeScript skills are reaching new heights!
Hereโs what you can explore next:
- Observer Pattern ๐: Learn how objects can watch and react to changes
- Strategy Pattern ๐ฏ: Master interchangeable algorithms
- Iterator Pattern ๐: Navigate through collections like a pro
Keep practicing with the Command Pattern by:
- Building an undo/redo system for a form ๐
- Creating a macro recorder for automation ๐ค
- Implementing a command-line interface ๐ป
Youโre doing amazing! The Command Pattern is now part of your TypeScript toolkit! ๐จโจ