+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 17 of 355

๐Ÿ’ซ Rest Parameters and Spread Operator: Variable Arguments in TypeScript

Master rest parameters and spread operator in TypeScript to handle variable arguments and array/object manipulation like a pro ๐Ÿš€

๐ŸŒฑBeginner
25 min read

Prerequisites

  • Basic TypeScript function knowledge ๐Ÿ“
  • Understanding of arrays and objects โšก
  • TypeScript development environment ๐Ÿ’ป

What you'll learn

  • Master rest parameters for variable arguments ๐ŸŽฏ
  • Use spread operator with arrays and objects ๐Ÿ—๏ธ
  • Combine rest and spread effectively ๐Ÿ”
  • Apply patterns in real-world scenarios โœจ

๐ŸŽฏ Introduction

Welcome to the dynamic world of rest parameters and spread operator in TypeScript! ๐ŸŽ‰ In this guide, weโ€™ll explore how to handle variable-length arguments and manipulate arrays and objects with elegance.

Imagine youโ€™re organizing a party ๐ŸŽ‰ - you donโ€™t know exactly how many guests will come, but you want to welcome them all! Thatโ€™s what rest parameters do for your functions. And the spread operator? Itโ€™s like unpacking a suitcase ๐Ÿฆผ - taking everything inside and spreading it out!

By the end of this tutorial, youโ€™ll be spreading and gathering data like a TypeScript ninja! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Rest Parameters and Spread Operator

๐Ÿค” What Are Rest Parameters?

Rest parameters are like a magic basket ๐Ÿงบ that collects all remaining arguments into an array. Theyโ€™re represented by three dots ... before a parameter name:

function collectAll(...items: string[]) {
  // items is an array containing all arguments!
}

Think of it as saying โ€œgive me the rest of the arguments!โ€ ๐ŸŽ‰

๐Ÿ’ก What Is the Spread Operator?

The spread operator looks the same (...) but works in reverse - it unpacks elements from arrays or properties from objects:

const numbers = [1, 2, 3];
console.log(...numbers); // 1 2 3 - spreads the array!

Itโ€™s like opening a box ๐Ÿ“ฆ and taking everything out!

๐ŸŽฏ Why Use Them?

Hereโ€™s why these features are game-changers:

  1. Flexible Functions ๐ŸŒˆ: Accept any number of arguments
  2. Clean Array/Object Operations ๐Ÿงฝ: Copy, merge, and manipulate with ease
  3. Type Safety ๐Ÿ›ก๏ธ: TypeScript tracks types through operations
  4. Readable Code ๐Ÿ“–: Express intent clearly
  5. Modern Patterns ๐Ÿš€: Essential for contemporary JavaScript/TypeScript

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Rest Parameters Basics

Letโ€™s start with rest parameters:

// ๐ŸŽจ Basic rest parameter
function sum(...numbers: number[]): number {
  return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum(1, 2, 3));        // 6 โœ…
console.log(sum(1, 2, 3, 4, 5));  // 15 โœ…
console.log(sum());               // 0 โœ…

// ๐ŸŽ‰ Rest parameters with other parameters
function greetAll(greeting: string, ...names: string[]): void {
  names.forEach(name => {
    console.log(`${greeting}, ${name}! ๐Ÿ‘‹`);
  });
}

greetAll("Hello", "Alice", "Bob", "Charlie");
// Output:
// Hello, Alice! ๐Ÿ‘‹
// Hello, Bob! ๐Ÿ‘‹
// Hello, Charlie! ๐Ÿ‘‹

// ๐ŸŽฏ Type-safe rest parameters
function logMessages(level: "info" | "warn" | "error", ...messages: string[]): void {
  const emoji = {
    info: "โ„น๏ธ",
    warn: "โš ๏ธ",
    error: "โŒ"
  };
  
  console.log(`${emoji[level]} [${level.toUpperCase()}]`, ...messages);
}

logMessages("info", "Server started", "Port 3000");
logMessages("error", "Connection failed", "Retry in 5s");

๐ŸŽจ Spread Operator with Arrays

Now letโ€™s explore the spread operator:

// ๐Ÿš€ Array spreading basics
const fruits = ["๐ŸŽ", "๐ŸŒ", "๐Ÿ‡"];
const vegetables = ["๐Ÿฅ•", "๐Ÿฅฆ", "๐Ÿ†"];

// Combine arrays
const food = [...fruits, ...vegetables];
console.log(food); // ["๐ŸŽ", "๐ŸŒ", "๐Ÿ‡", "๐Ÿฅ•", "๐Ÿฅฆ", "๐Ÿ†"]

// ๐ŸŽจ Copy arrays (no mutation!)
const originalScores = [100, 85, 92];
const copiedScores = [...originalScores];
copiedScores.push(88);

console.log(originalScores); // [100, 85, 92] โœ… Original unchanged!
console.log(copiedScores);   // [100, 85, 92, 88]

// ๐ŸŽฏ Insert elements
const numbers = [1, 2, 5, 6];
const complete = [...numbers.slice(0, 2), 3, 4, ...numbers.slice(2)];
console.log(complete); // [1, 2, 3, 4, 5, 6]

// ๐Ÿ—๏ธ Convert iterables to arrays
function lettersToArray(word: string): string[] {
  return [...word]; // Spreads string into character array!
}

console.log(lettersToArray("Hello")); // ["H", "e", "l", "l", "o"]

๐Ÿ”„ Spread Operator with Objects

// ๐Ÿ  Object spreading basics
interface User {
  name: string;
  age: number;
  email: string;
}

const baseUser: User = {
  name: "Alice",
  age: 30,
  email: "[email protected]"
};

// ๐ŸŽจ Create a copy with updates
const updatedUser = {
  ...baseUser,
  age: 31,
  emoji: "๐Ÿ˜Š"
};

console.log(baseUser);    // Original unchanged
console.log(updatedUser); // New object with updates

// ๐ŸŽฏ Merge objects
const defaults = {
  theme: "light",
  language: "en",
  notifications: true
};

const userPrefs = {
  theme: "dark",
  fontSize: 16
};

const finalSettings = {
  ...defaults,
  ...userPrefs  // Later spreads override earlier ones
};
// Result: { theme: "dark", language: "en", notifications: true, fontSize: 16 }

// ๐Ÿ›ก๏ธ Type-safe object composition
interface Animal {
  name: string;
  species: string;
}

interface Pet extends Animal {
  owner: string;
  trained: boolean;
}

const animal: Animal = { name: "Max", species: "dog" };
const pet: Pet = {
  ...animal,
  owner: "Sarah",
  trained: true
};

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: Flexible Shopping Cart

Letโ€™s build a shopping cart that accepts multiple items at once:

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

class ShoppingCart {
  private items: Product[] = [];
  
  // ๐ŸŽฏ Add multiple items at once using rest parameters
  addItems(...products: Product[]): void {
    this.items.push(...products); // Spread to add all!
    console.log(`๐Ÿ›’ Added ${products.length} items to cart!`);
    products.forEach(p => console.log(`  ${p.emoji} ${p.name}`));
  }
  
  // ๐Ÿ’ฐ Calculate total with optional tax rates
  calculateTotal(...taxRates: number[]): number {
    const subtotal = this.items.reduce((sum, item) => sum + item.price, 0);
    const totalTaxRate = taxRates.reduce((sum, rate) => sum + rate, 0) / 100;
    const tax = subtotal * totalTaxRate;
    
    console.log(`๐Ÿ“Š Subtotal: $${subtotal.toFixed(2)}`);
    if (taxRates.length > 0) {
      console.log(`๐Ÿ“‹ Tax (${totalTaxRate * 100}%): $${tax.toFixed(2)}`);
    }
    console.log(`๐Ÿ’ณ Total: $${(subtotal + tax).toFixed(2)}`);
    
    return subtotal + tax;
  }
  
  // ๐Ÿท๏ธ Filter by categories
  filterByCategories(...categories: string[]): Product[] {
    return this.items.filter(item => 
      categories.includes(item.category)
    );
  }
  
  // ๐Ÿ  Clone cart with modifications
  cloneAndModify(modifications: Partial<Product>): ShoppingCart {
    const newCart = new ShoppingCart();
    const modifiedItems = this.items.map(item => ({
      ...item,
      ...modifications
    }));
    newCart.addItems(...modifiedItems);
    return newCart;
  }
}

// ๐ŸŽฎ Usage
const cart = new ShoppingCart();

// Add multiple items at once
cart.addItems(
  { id: "1", name: "TypeScript Handbook", price: 39.99, category: "books", emoji: "๐Ÿ“˜" },
  { id: "2", name: "Mechanical Keyboard", price: 149.99, category: "electronics", emoji: "โŒจ๏ธ" },
  { id: "3", name: "Coffee Beans", price: 15.99, category: "food", emoji: "โ˜•" }
);

// Calculate with multiple tax rates
cart.calculateTotal(8.5, 2); // State tax + local tax

// Filter electronics
const electronics = cart.filterByCategories("electronics", "computers");
console.log(`\n๐Ÿ“ฑ Electronics:`, electronics.map(e => e.emoji));

๐ŸŽจ Example 2: Event System with Rest & Spread

Letโ€™s create a flexible event system:

// ๐ŸŽ† Event system with variable arguments
type EventCallback = (...args: any[]) => void;

class EventEmitter {
  private events: Map<string, EventCallback[]> = new Map();
  
  // ๐Ÿ“ข Subscribe to events
  on(event: string, ...callbacks: EventCallback[]): void {
    if (!this.events.has(event)) {
      this.events.set(event, []);
    }
    
    const handlers = this.events.get(event)!;
    handlers.push(...callbacks); // Spread to add all callbacks
    
    console.log(`๐ŸŽ‰ Subscribed ${callbacks.length} handlers to "${event}"`);
  }
  
  // ๐Ÿ’ฅ Emit events with any number of arguments
  emit(event: string, ...args: any[]): void {
    const handlers = this.events.get(event) || [];
    
    console.log(`๐Ÿ“ก Emitting "${event}" to ${handlers.length} handlers`);
    handlers.forEach(handler => {
      handler(...args); // Spread arguments to handler
    });
  }
  
  // ๐Ÿ—บ๏ธ Create a filtered emitter
  createFilteredEmitter(...allowedEvents: string[]): EventEmitter {
    const filtered = new EventEmitter();
    
    // Copy only allowed events
    allowedEvents.forEach(event => {
      const handlers = this.events.get(event);
      if (handlers) {
        filtered.on(event, ...handlers);
      }
    });
    
    return filtered;
  }
}

// ๐ŸŽฎ Usage
const gameEvents = new EventEmitter();

// Subscribe multiple handlers at once
gameEvents.on("player-join",
  (name: string) => console.log(`๐ŸŽฎ ${name} joined the game!`),
  (name: string) => console.log(`๐ŸŽ† Welcome ${name}!`),
  (name: string) => console.log(`๐Ÿ‘ฅ Players online: updating...`)
);

gameEvents.on("score-update",
  (player: string, points: number, bonus?: string) => {
    console.log(`๐Ÿ† ${player} scored ${points} points!`);
    if (bonus) console.log(`  ๐ŸŽ† Bonus: ${bonus}`);
  }
);

// Emit with variable arguments
gameEvents.emit("player-join", "Alice");
gameEvents.emit("score-update", "Alice", 100, "First Kill!");

๐ŸŽฎ Example 3: Configuration Merger

Letโ€™s build a powerful configuration system:

// ๐Ÿ”ง Deep configuration merger
interface Config {
  [key: string]: any;
}

class ConfigManager {
  // ๐ŸŽจ Merge multiple configs with precedence
  static merge(...configs: Config[]): Config {
    return configs.reduce((merged, config) => {
      return this.deepMerge(merged, config);
    }, {});
  }
  
  // ๐Ÿ—๏ธ Deep merge helper
  private static deepMerge(target: Config, source: Config): Config {
    const result = { ...target }; // Spread to copy
    
    Object.keys(source).forEach(key => {
      if (typeof source[key] === 'object' && !Array.isArray(source[key])) {
        result[key] = this.deepMerge(result[key] || {}, source[key]);
      } else if (Array.isArray(source[key])) {
        result[key] = [...(result[key] || []), ...source[key]]; // Merge arrays
      } else {
        result[key] = source[key];
      }
    });
    
    return result;
  }
  
  // ๐Ÿ” Create environment-specific config
  static createEnvConfig(base: Config, ...environments: Array<[string, Config]>): Map<string, Config> {
    const envConfigs = new Map<string, Config>();
    
    environments.forEach(([env, config]) => {
      envConfigs.set(env, this.merge(base, config));
      console.log(`๐ŸŽฏ Created config for ${env} environment`);
    });
    
    return envConfigs;
  }
}

// ๐Ÿš€ Usage
const defaultConfig = {
  app: {
    name: "MyApp",
    version: "1.0.0",
    features: ["basic"]
  },
  server: {
    port: 3000,
    host: "localhost"
  },
  database: {
    type: "postgres",
    pool: { min: 2, max: 10 }
  }
};

const productionConfig = {
  server: {
    port: 443,
    host: "api.myapp.com",
    ssl: true
  },
  database: {
    pool: { min: 10, max: 50 },
    ssl: { rejectUnauthorized: true }
  },
  app: {
    features: ["premium", "analytics"]
  }
};

const developmentConfig = {
  server: {
    debug: true
  },
  app: {
    features: ["debug-panel"]
  }
};

// Merge configs
const finalConfig = ConfigManager.merge(
  defaultConfig,
  productionConfig,
  { app: { emoji: "๐Ÿš€" } }
);

console.log("๐ŸŽจ Final config:", finalConfig);

// Create environment configs
const envConfigs = ConfigManager.createEnvConfig(
  defaultConfig,
  ["production", productionConfig],
  ["development", developmentConfig]
);

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Generic Rest Parameters

Level up with type-safe generic rest parameters:

// ๐ŸŽฏ Type-safe pipe function
function pipe<T extends any[], R>(
  fn1: (...args: T) => R
): (...args: T) => R;
function pipe<T extends any[], R1, R2>(
  fn1: (...args: T) => R1,
  fn2: (arg: R1) => R2
): (...args: T) => R2;
function pipe<T extends any[], R1, R2, R3>(
  fn1: (...args: T) => R1,
  fn2: (arg: R1) => R2,
  fn3: (arg: R2) => R3
): (...args: T) => R3;
function pipe(...fns: Function[]) {
  return (...args: any[]) => {
    return fns.reduce((result, fn, index) => {
      return index === 0 ? fn(...args) : fn(result);
    }, undefined);
  };
}

// ๐Ÿš€ Usage
const addTax = (price: number) => price * 1.2;
const format = (price: number) => `$${price.toFixed(2)}`;
const addEmoji = (price: string) => `๐Ÿ’ฐ ${price}`;

const priceProcessor = pipe(addTax, format, addEmoji);
console.log(priceProcessor(100)); // "๐Ÿ’ฐ $120.00"

// ๐ŸŽจ Type-safe tuple spread
type Head<T extends readonly unknown[]> = T extends readonly [infer H, ...unknown[]] ? H : never;
type Tail<T extends readonly unknown[]> = T extends readonly [unknown, ...infer Rest] ? Rest : [];

function first<T extends readonly unknown[]>(...args: T): Head<T> {
  return args[0] as Head<T>;
}

function rest<T extends readonly unknown[]>(...args: T): Tail<T> {
  const [, ...tail] = args;
  return tail as Tail<T>;
}

// Usage
const head = first(1, "hello", true); // type: number
const tail = rest(1, "hello", true);  // type: [string, boolean]

๐Ÿ—๏ธ Conditional Types with Spread

Combine spread with advanced type manipulation:

// ๐Ÿงฌ Deep partial type using spread
type DeepPartial<T> = T extends object ? {
  [P in keyof T]?: DeepPartial<T[P]>;
} : T;

// ๐ŸŽจ Merge function with deep type safety
function deepMerge<T extends object, U extends DeepPartial<T>>(
  target: T,
  ...sources: U[]
): T & U {
  const result = { ...target };
  
  sources.forEach(source => {
    Object.keys(source).forEach(key => {
      const sourceValue = source[key as keyof U];
      const targetValue = result[key as keyof T];
      
      if (typeof sourceValue === 'object' && typeof targetValue === 'object') {
        (result as any)[key] = deepMerge(targetValue as any, sourceValue as any);
      } else {
        (result as any)[key] = sourceValue;
      }
    });
  });
  
  return result as T & U;
}

// ๐Ÿš€ Usage
interface AppConfig {
  theme: {
    colors: {
      primary: string;
      secondary: string;
    };
    fonts: string[];
  };
  features: {
    analytics: boolean;
    notifications: boolean;
  };
}

const baseConfig: AppConfig = {
  theme: {
    colors: { primary: "blue", secondary: "gray" },
    fonts: ["Arial", "Helvetica"]
  },
  features: {
    analytics: true,
    notifications: false
  }
};

const merged = deepMerge(baseConfig, 
  { theme: { colors: { primary: "red" } } },
  { features: { notifications: true } }
);

๐ŸŽจ Variadic Tuple Types

// ๐ŸŽฏ Advanced tuple manipulation
type Concat<T extends readonly unknown[], U extends readonly unknown[]> = [...T, ...U];
type Zip<T extends readonly unknown[], U extends readonly unknown[]> = {
  [K in keyof T]: K extends keyof U ? [T[K], U[K]] : never;
};

// ๐Ÿ—บ๏ธ Utility functions using variadic tuples
function concat<T extends readonly unknown[], U extends readonly unknown[]>(
  arr1: T,
  arr2: U
): Concat<T, U> {
  return [...arr1, ...arr2] as Concat<T, U>;
}

function zip<T extends readonly unknown[], U extends readonly unknown[]>(
  arr1: T,
  arr2: U
): Zip<T, U> {
  return arr1.map((item, index) => 
    [item, arr2[index]]
  ) as Zip<T, U>;
}

// Usage with type inference
const combined = concat([1, 2] as const, ["a", "b"] as const);
// Type: readonly [1, 2, "a", "b"]

const paired = zip(["x", "y", "z"] as const, [1, 2, 3] as const);
// Type: readonly [["x", 1], ["y", 2], ["z", 3]]

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Rest Parameter Position

// โŒ Wrong - rest parameter must be last!
function badFunction(...items: string[], lastItem: number): void {
  // TypeScript Error: A rest parameter must be last in a parameter list
}

// โœ… Correct - rest parameter at the end
function goodFunction(firstItem: number, ...items: string[]): void {
  console.log(`First: ${firstItem}, Rest: ${items.join(", ")}`);
}

// ๐ŸŽฏ Remember: Only ONE rest parameter allowed
function process(category: string, ...items: string[]): void {
  // Perfect! Category first, then collect all items
}

๐Ÿคฏ Pitfall 2: Mutation vs Immutability

// โŒ Dangerous - mutating original array
const original = [1, 2, 3];
const extended = original; // This is NOT a copy!
extended.push(4);
console.log(original); // [1, 2, 3, 4] ๐Ÿ˜ฑ Original changed!

// โœ… Safe - using spread for immutability
const original2 = [1, 2, 3];
const extended2 = [...original2, 4]; // Creates new array
console.log(original2); // [1, 2, 3] โœ… Original unchanged!

// ๐ŸŽจ Same with objects
const user = { name: "Alice", age: 30 };
// โŒ Wrong
const updated = user;
updated.age = 31; // Mutates original!

// โœ… Correct
const updated2 = { ...user, age: 31 }; // New object

๐Ÿ˜ต Pitfall 3: Spread Order Matters

// โš ๏ธ Order affects final values
const defaults = { theme: "light", fontSize: 14, debug: false };
const userPrefs = { theme: "dark", debug: true };

// Different results based on order!
const config1 = { ...defaults, ...userPrefs };
// Result: { theme: "dark", fontSize: 14, debug: true }

const config2 = { ...userPrefs, ...defaults };
// Result: { theme: "light", fontSize: 14, debug: false } ๐Ÿ˜ฑ

// โœ… Be explicit about precedence
const finalConfig = {
  ...defaults,      // Base configuration
  ...userPrefs,     // User overrides
  version: "1.0.0"  // Additional properties
};

๐Ÿค” Pitfall 4: Type Loss with Spread

// โš ๏ธ TypeScript may lose type information
const numbers = [1, 2, 3] as const;
const moreNumbers = [...numbers, 4]; // Type: number[], not [1, 2, 3, 4]

// โœ… Preserve types when needed
const tuple = [1, "hello", true] as const;
const preservedTuple = [...tuple] as const; // Keeps readonly tuple type

// ๐ŸŽจ Function overloads for type preservation
function combine<T extends readonly unknown[], U extends readonly unknown[]>(
  arr1: T,
  arr2: U
): [...T, ...U] {
  return [...arr1, ...arr2];
}

const combined = combine([1, 2] as const, ["a", "b"] as const);
// Type: [1, 2, "a", "b"]

๐Ÿ˜ฌ Pitfall 5: Deep vs Shallow Copy

// โŒ Spread only does shallow copy!
const nested = {
  user: { name: "Alice", settings: { theme: "dark" } },
  scores: [100, 200]
};

const copied = { ...nested };
copied.user.settings.theme = "light";
console.log(nested.user.settings.theme); // "light" ๐Ÿ˜ฑ Original changed!

// โœ… Deep copy solution
function deepCopy<T>(obj: T): T {
  if (obj === null || typeof obj !== "object") return obj;
  if (obj instanceof Date) return new Date(obj.getTime()) as any;
  if (Array.isArray(obj)) return obj.map(item => deepCopy(item)) as any;
  
  const cloned = {} as T;
  for (const key in obj) {
    cloned[key] = deepCopy(obj[key]);
  }
  return cloned;
}

const deepCopied = deepCopy(nested);
deepCopied.user.settings.theme = "light";
console.log(nested.user.settings.theme); // "dark" โœ… Original unchanged!

๐Ÿ› ๏ธ Best Practices

1. ๐ŸŽฏ Use Rest Parameters for Flexibility

// โœ… Good - flexible API
function log(level: string, ...messages: unknown[]): void {
  console.log(`[${level}]`, ...messages);
}

// Usage is intuitive
log("INFO", "Server started");
log("ERROR", "Failed to connect", { code: 500 }, new Error());

2. ๐Ÿ“ Preserve Immutability with Spread

// โœ… Good - always create new objects/arrays
const addItem = <T>(array: readonly T[], item: T): T[] => {
  return [...array, item]; // New array
};

const updateUser = (user: User, updates: Partial<User>): User => {
  return { ...user, ...updates }; // New object
};

3. ๐Ÿ›ก๏ธ Type Rest Parameters Properly

// โŒ Avoid any[]
function bad(...args: any[]): void { }

// โœ… Be specific
function good(...args: string[]): void { }
function better(...args: Array<string | number>): void { }
function best<T>(...args: T[]): void { }

4. ๐ŸŽจ Use Const Assertions for Tuples

// โœ… Preserve literal types
const point = [10, 20] as const; // readonly [10, 20]
const config = { host: "localhost", port: 3000 } as const;

// Spread preserves the types
const newPoint = [...point, 30] as const; // readonly [10, 20, 30]

5. โœจ Combine Patterns Effectively

// โœ… Rest + spread + defaults = powerful APIs
class Logger {
  constructor(
    private defaultTags: string[] = []
  ) {}
  
  log(message: string, ...additionalTags: string[]): void {
    const allTags = [...this.defaultTags, ...additionalTags];
    console.log(`[${allTags.join(", ")}] ${message}`);
  }
}

const logger = new Logger(["app", "v1"]);
logger.log("Started", "startup", "info");
// Output: [app, v1, startup, info] Started

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Flexible Command System

Create a type-safe command system using rest parameters and spread operator:

๐Ÿ“‹ Requirements:

  • โœ… Commands with variable arguments
  • ๐Ÿท๏ธ Command chaining and composition
  • ๐Ÿ‘ค Middleware support
  • ๐Ÿ“… Command history with undo
  • ๐ŸŽจ Type-safe command parsing

๐Ÿš€ Bonus Points:

  • Add command aliases
  • Implement command piping
  • Create batch execution

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
// ๐ŸŽฏ Flexible command system with rest & spread
type CommandHandler = (...args: string[]) => void | Promise<void>;
type Middleware = (args: string[], next: () => void) => void;

interface Command {
  name: string;
  aliases: string[];
  handler: CommandHandler;
  description: string;
}

class CommandSystem {
  private commands = new Map<string, Command>();
  private history: Array<{ command: string; args: string[] }> = [];
  private middlewares: Middleware[] = [];
  
  // ๐Ÿ“ Register commands with aliases
  register(name: string, handler: CommandHandler, ...aliases: string[]): this {
    const command: Command = {
      name,
      aliases,
      handler,
      description: ""
    };
    
    // Register main command and all aliases
    this.commands.set(name, command);
    aliases.forEach(alias => this.commands.set(alias, command));
    
    console.log(`โœจ Registered command: ${name} ${aliases.length > 0 ? `(aliases: ${aliases.join(", ")})` : ""}`);
    return this;
  }
  
  // ๐ŸŽจ Add middleware
  use(...middlewares: Middleware[]): this {
    this.middlewares.push(...middlewares);
    return this;
  }
  
  // ๐Ÿš€ Execute command with arguments
  async execute(input: string): Promise<void> {
    const [commandName, ...args] = input.split(" ");
    const command = this.commands.get(commandName);
    
    if (!command) {
      console.log(`โŒ Unknown command: ${commandName}`);
      return;
    }
    
    // Save to history
    this.history.push({ command: commandName, args });
    
    // Run through middlewares
    let index = 0;
    const next = (): void => {
      if (index < this.middlewares.length) {
        const middleware = this.middlewares[index++];
        middleware(args, next);
      } else {
        // Execute the actual command
        console.log(`๐ŸŽฎ Executing: ${commandName} ${args.join(" ")}`);
        command.handler(...args);
      }
    };
    
    next();
  }
  
  // ๐Ÿ”„ Batch execute multiple commands
  async batch(...commands: string[]): Promise<void> {
    console.log(`๐ŸŽจ Batch executing ${commands.length} commands`);
    for (const cmd of commands) {
      await this.execute(cmd);
    }
  }
  
  // ๐Ÿ—๏ธ Create command pipeline
  pipe(...commandStrings: string[]): (...initialArgs: string[]) => void {
    return (...initialArgs: string[]) => {
      let result = initialArgs;
      
      commandStrings.forEach(cmdStr => {
        const [cmdName, ...cmdArgs] = cmdStr.split(" ");
        const command = this.commands.get(cmdName);
        
        if (command) {
          // Pass previous result as additional arguments
          command.handler(...cmdArgs, ...result);
        }
      });
    };
  }
  
  // ๐Ÿ“Š Show command history
  showHistory(limit?: number): void {
    const historyToShow = limit ? this.history.slice(-limit) : this.history;
    console.log("\n๐Ÿ“œ Command History:");
    historyToShow.forEach((entry, index) => {
      console.log(`  ${index + 1}. ${entry.command} ${entry.args.join(" ")}`);
    });
  }
  
  // ๐Ÿ”™ Undo last command (simplified)
  undo(): void {
    const last = this.history.pop();
    if (last) {
      console.log(`โ†ฉ๏ธ Undoing: ${last.command} ${last.args.join(" ")}`);
    }
  }
}

// ๐ŸŽฎ Usage Example
const cli = new CommandSystem();

// Register commands with rest parameters
cli.register("echo", (...args) => {
  console.log(`๐Ÿ“ข ${args.join(" ")}`);
}, "print", "say");

cli.register("add", (...numbers) => {
  const sum = numbers.reduce((acc, n) => acc + parseFloat(n), 0);
  console.log(`โž• Sum: ${sum}`);
});

cli.register("concat", (...strings) => {
  console.log(`๐Ÿ”— ${strings.join("")}`)
});

cli.register("list", (...items) => {
  console.log("๐Ÿ“‹ List:");
  items.forEach((item, i) => console.log(`  ${i + 1}. ${item}`));
});

// Add middleware
cli.use(
  (args, next) => {
    console.log(`๐Ÿ” Middleware: Processing ${args.length} arguments`);
    next();
  },
  (args, next) => {
    if (args.some(arg => arg.includes("error"))) {
      console.log(`โš ๏ธ Warning: Arguments contain 'error'`);
    }
    next();
  }
);

// Execute commands
await cli.execute("echo Hello TypeScript World");
await cli.execute("add 10 20 30 40");
await cli.execute("list apple banana cherry date");

// Batch execution
await cli.batch(
  "echo Starting batch",
  "add 1 2 3",
  "concat Type Script Rocks",
  "echo Batch complete"
);

// Create pipeline
const processData = cli.pipe("echo", "concat");
processData("Hello", "World");

// Show history
cli.showHistory(5);

// Test aliases
await cli.execute("say This uses an alias!");
await cli.execute("print Another alias test");

๐ŸŽ“ Key Takeaways

Youโ€™ve mastered rest parameters and spread operator! Hereโ€™s what you can now do:

  • โœ… Handle variable arguments with type-safe rest parameters ๐Ÿ’ช
  • โœ… Clone and merge arrays/objects without mutations ๐Ÿ›ก๏ธ
  • โœ… Compose functions with flexible parameter handling ๐ŸŽฏ
  • โœ… Build flexible APIs that users love ๐Ÿ›
  • โœ… Write cleaner code with modern JavaScript patterns! ๐Ÿš€

Remember: Rest collects, spread expands! These tools make your code more flexible and maintainable. ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered rest parameters and spread operator!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Complete the command system exercise above
  2. ๐Ÿ—๏ธ Refactor existing code to use spread for immutability
  3. ๐Ÿ“š Move on to our next tutorial: Function Overloading
  4. ๐ŸŒŸ Create flexible APIs in your projects!

Remember: These patterns are everywhere in modern TypeScript. The more you use them, the more natural they become! ๐Ÿš€

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