Skip to content

Options with Defaults

When to use

Use this pattern when a function or class accepts an optional configuration object and needs to fill in missing values from a defaults object. This is the standard pattern for API design in JavaScript — callers provide only the options they care about.

The pattern

Spread merge (preferred for plain objects)

Merge user options over defaults with object spread. Later properties win, so user-supplied values override defaults:

javascript
const DEFAULTS = {
  timeout: 5000,
  retries: 3,
  verbose: false,
};

function createClient(options = {}) {
  const resolved = { ...DEFAULTS, ...options };
  // resolved.timeout is options.timeout if provided,
  // otherwise 5000
  return resolved;
}

This one-liner — { ...defaults, ...options } — is the cleanest way to merge configuration when every value in options should unconditionally override the corresponding default.

Static defaults with nullish coalescing

When defaults live on a class (common in web components) and values come from attributes that may be null, use nullish coalescing (??) per property:

javascript
class UiIcon extends HTMLElement {
  static defaults = {
    library: "lucide",
    type: "filled",
    size: "24",
    color: "currentColor",
    strokeWidth: "2",
  };
  
  get size() {
    return this.getAttribute("size")
      ?? UiIcon.defaults.size;
  }
  
  get color() {
    return this.getAttribute("color")
      ?? UiIcon.defaults.color;
  }
  
  setStyleValues() {
    const defaults = UiIcon.defaults;
    const size = this.getAttribute("size")
      ?? defaults.size;
    this.style.setProperty("--size", size);
    
    const color = this.getAttribute("color")
      ?? defaults.color;
    this.style.setProperty("--color", color);
  }
}

Use ?? (not ||) so that "" and 0 are treated as intentional values rather than falling through to the default.

Spread merge in a function call

The same { ...defaults, ...options } pattern works inside a function that accepts caller overrides. Callers supply only the options they care about:

javascript
function request(url, options = {}) {
  const config = {
    method: "GET",
    headers: {},
    timeout: 10000,
    ...options,
  };
  return fetch(url, config);
}

// Caller overrides only what they need
request("/api/data", { timeout: 30000 });

Nested defaults

Spread is shallow — nested objects are replaced, not merged. For nested configuration, merge each level explicitly:

javascript
const DEFAULTS = {
  server: { host: "localhost", port: 3000 },
  logging: { level: "info", pretty: false },
};

function configure(options = {}) {
  return {
    server: { ...DEFAULTS.server, ...options.server },
    logging: { ...DEFAULTS.logging, ...options.logging },
  };
}

Combining with filterObject

When callers might pass unexpected keys, filter first, then merge with defaults:

javascript
function createPool(options = {}) {
  const allowed = filterObject(options, [
    "host", "port", "user", "password", "database",
  ]);
  return mysql.createPool({ ...POOL_DEFAULTS, ...allowed });
}

Trade-offs

  • Spread merge — simple, declarative, one line. Does not handle nested objects (shallow only). Values like undefined in options still override defaults because the property exists on the spread source.
  • Nullish coalescing — more verbose, but handles per-property fallback chains where values come from different sources (attributes, environment, static defaults). Only falls through on null/undefined, not on "" or 0.
  • Object.assign — mutates the target. Prefer spread. Object.assign(DEFAULTS, options) overwrites DEFAULTS — usually a bug. Use Object.assign({}, DEFAULTS, options) to create a safe copy.
  • Deep merge libraries — use lodash.merge or similar only when the config schema has arbitrary nesting depth. For known shapes, explicit per-level spread is clearer and has no dependency.