Appearance
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
undefinedinoptionsstill 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""or0. Object.assign— mutates the target. Prefer spread.Object.assign(DEFAULTS, options)overwritesDEFAULTS— usually a bug. UseObject.assign({}, DEFAULTS, options)to create a safe copy.- Deep merge libraries — use
lodash.mergeor similar only when the config schema has arbitrary nesting depth. For known shapes, explicit per-level spread is clearer and has no dependency.