When a function takes more than 3 parameters, readability drops sharply. The Introduce Parameter Object refactoring groups related parameters into a cohesive object, improving readability, type safety, and extensibility.

The Problem

// ❌ What do these parameters mean at the call site?
function searchProducts(
  query: string,
  category: string | null,
  minPrice: number,
  maxPrice: number,
  sortBy: string,
  sortOrder: 'asc' | 'desc',
  page: number,
  pageSize: number,
  inStock: boolean,
  brand: string | null,
): Promise<Product[]> {
  // ...
}

// Calling this is a nightmare
const results = await searchProducts(
  'laptop',    // query
  'electronics', // category
  500,         // min price? max price? who knows
  2000,        // the other one
  'price',     // sort by
  'asc',       // sort order
  1,           // page
  20,          // page size
  true,        // in stock
  null,        // brand
);

The Refactoring

// ✅ Group related parameters into meaningful objects
interface ProductSearchCriteria {
  query: string;
  category?: string;
  priceRange?: {
    min: number;
    max: number;
  };
  brand?: string;
  inStock?: boolean;
}

interface SortOptions {
  field: string;
  order: 'asc' | 'desc';
}

interface PaginationOptions {
  page: number;
  pageSize: number;
}

interface SearchOptions {
  criteria: ProductSearchCriteria;
  sort?: SortOptions;
  pagination?: PaginationOptions;
}

async function searchProducts(options: SearchOptions): Promise<ProductSearchResult> {
  const { criteria, sort, pagination } = options;
  // Clean, named access to everything
  // ...
}

// The call site is now self-documenting
const results = await searchProducts({
  criteria: {
    query: 'laptop',
    category: 'electronics',
    priceRange: { min: 500, max: 2000 },
    inStock: true,
  },
  sort: { field: 'price', order: 'asc' },
  pagination: { page: 1, pageSize: 20 },
});

Benefits

1. Self-Documenting Call Sites

Every value has a name. No more guessing what true or 20 means.

2. Optional Parameters Without Ambiguity

// Just omit what you don't need
const results = await searchProducts({
  criteria: { query: 'laptop' },
});

3. Easy to Extend

// Adding a new filter? Just add a property — no signature change
interface ProductSearchCriteria {
  query: string;
  category?: string;
  priceRange?: { min: number; max: number };
  brand?: string;
  inStock?: boolean;
  rating?: number;           // New!
  freeShipping?: boolean;    // New!
}
// Zero changes to existing call sites

4. Reusable Types

// The same pagination type works everywhere
interface PaginationOptions {
  page: number;
  pageSize: number;
}

function searchProducts(opts: { criteria: ProductSearchCriteria; pagination?: PaginationOptions }) { }
function searchUsers(opts: { query: string; pagination?: PaginationOptions }) { }
function searchOrders(opts: { filters: OrderFilters; pagination?: PaginationOptions }) { }

Builder Pattern for Complex Options

When the object itself is complex, combine with a builder:

class SearchBuilder {
  private options: SearchOptions = { criteria: { query: '' } };

  query(q: string): this {
    this.options.criteria.query = q;
    return this;
  }

  category(cat: string): this {
    this.options.criteria.category = cat;
    return this;
  }

  priceRange(min: number, max: number): this {
    this.options.criteria.priceRange = { min, max };
    return this;
  }

  sortBy(field: string, order: 'asc' | 'desc' = 'asc'): this {
    this.options.sort = { field, order };
    return this;
  }

  page(page: number, size: number = 20): this {
    this.options.pagination = { page, pageSize: size };
    return this;
  }

  build(): SearchOptions {
    return structuredClone(this.options);
  }
}

// Fluent, readable API
const results = await searchProducts(
  new SearchBuilder()
    .query('laptop')
    .category('electronics')
    .priceRange(500, 2000)
    .sortBy('price', 'asc')
    .page(1)
    .build()
);

Rule of Thumb

  • 1-2 parameters: Keep as-is
  • 3 parameters: Consider grouping if they’re related
  • 4+ parameters: Almost always better as an object
  • Boolean flags: Always better named ({ verbose: true } vs true)

“The ideal number of arguments for a function is zero.” — Robert C. Martin, Clean Code