Combine configuration objects without rewriting precedence and array rules at every call site.
| Concern | Without it | With smob |
|---|---|---|
| Nested defaults | A spread or Object.assign() replaces a nested object |
Recursively combine nested object properties |
| Precedence | Order each assignment yourself | Leftmost wins by default; configure priority |
| Arrays | Implement concatenation or replacement per field | Configure array merging, order and deduplication |
| Isolation | Clone inputs separately when references must not be shared | Enable clone for deep-cloned inputs |
| Custom conflicts | Add a second pass over merged keys | Handle an existing key in strategy |
import { merge } from 'smob';
const config = { port: 3000, db: { host: 'local' }, tags: ['app'] };
const defaults = { port: 80, db: { pool: 5 }, tags: ['defaults'] };
console.log({ ...defaults, ...config }.db); // { host: 'local' }
console.log(merge(config, defaults));
// { port: 3000, db: { host: 'local', pool: 5 }, tags: ['app', 'defaults'] }Table of Contents
npm install smobRequires Node.js 20 or newer. ESM, CommonJS and TypeScript declarations are included. There are no runtime dependencies.
Call merge(...sources) with one or more arrays or objects (MergerSource[]). The result combines their values according to the default options:
import { merge } from 'smob';
console.log(merge({ a: 1 }, { b: 2 }, { c: 3 })); // { a: 1, b: 2, c: 3 }
console.log(merge(['foo'], ['bar'])); // ['foo', 'bar']Create a reusable merger with createMerger(options). For example, disable property-array merging or remove duplicate array elements:
import { createMerger } from 'smob';
const keepFirstArray = createMerger({ array: false });
console.log(keepFirstArray({ a: [1, 2, 3] }, { a: [4, 5, 6] }));
// { a: [1, 2, 3] }
const distinct = createMerger({ arrayDistinct: true });
console.log(distinct({ a: [1, 2, 3] }, { a: [3, 4, 5] }));
// { a: [1, 2, 3, 4, 5] }
const rightmostWins = createMerger({ priority: 'right' });
console.log(rightmostWins({ a: 1 }, { a: 2 }, { a: 3 })); // { a: 3 }import { createMerger } from 'smob';
const merge = createMerger({ priority: 'right', arrayPriority: 'left', arrayDistinct: true });
console.log(merge({ port: 80, tags: ['base'] }, { port: 3000, tags: ['base', 'app'] }));
// { port: 3000, tags: ['base', 'app'] }
const replaceArrays = createMerger({ array: false });
console.log(replaceArrays({ tags: ['app'] }, { tags: ['defaults'] }));
// { tags: ['app'] }array: false controls array-valued object properties. Top-level array inputs still concatenate. arrayDistinct removes equal elements using isEqual().
import { createMerger } from 'smob';
const sum = createMerger({
strategy: (target, key, value) => {
if (typeof target[key] === 'number' && typeof value === 'number') {
target[key] += value;
return target;
}
return undefined;
},
});
console.log(sum({ count: 1 }, { count: 2 }, { count: 3 })); // { count: 6 }A strategy runs for keys already present on the target. Mutate target and return a defined value to mark the conflict handled. The return value is a signal; it does not replace the target. Return undefined to let the standard merge continue.
import { assign, distinctArray, isEqual } from 'smob';
const target = { port: 3000 };
console.log(assign(target, { port: 80, host: 'local' })); // { port: 3000, host: 'local' }
console.log(distinctArray([{ id: 1 }, { id: 1 }])); // [{ id: 1 }]
console.log(isEqual({ tags: ['app'] }, { tags: ['app'] })); // trueassign() mutates the target, keeps its existing values and disables merging array-valued properties. Its precedence differs from Object.assign(). distinctArray() also mutates its input.
Pass options to createMerger(options) once, then call the returned merger with one or more sources.
| Option | Type | Default | Behavior |
|---|---|---|---|
priority |
`'left' | 'right'` | 'left' |
array |
boolean |
true |
Concatenate array-valued object properties |
arrayPriority |
`'left' | 'right'` | priority |
arrayDistinct |
boolean |
false |
Remove deeply equal array elements |
inPlace |
boolean |
false |
Use the priority-selected source as the target |
clone |
boolean |
false |
Deep-clone sources before merging; takes precedence over inPlace |
strategy |
`(target, key, value) => object | undefined` | undefined |
Remove repeated primitives, objects or nested arrays using deep equality. The function changes the supplied array and returns that same array:
import { distinctArray } from 'smob';
console.log(distinctArray(['foo', 'bar', 'foo'])); // ['foo', 'bar']
console.log(distinctArray([{ foo: 'bar' }, { foo: 'bar' }])); // [{ foo: 'bar' }]
console.log(distinctArray([['foo', 'bar'], ['foo', 'bar']])); // [['foo', 'bar']]Compare values by reference/value and recursively compare object properties or array elements. Date values compare timestamps, and RegExp values compare their string representations:
import { isEqual } from 'smob';
console.log(isEqual({ foo: 'bar' }, { foo: 'bar' })); // true
console.log(isEqual(['foo', 'bar'], ['foo', 'bar'])); // trueAvoid deep equality and deduplication on cyclic structures; these helpers have no cycle detection.
All exports are named exports from smob.
| Export | Kind | Purpose |
|---|---|---|
merge(...sources) |
Function | Merger with the defaults above |
createMerger(options?) |
Function | Construct a reusable configured merger |
assign(target, ...sources) |
Function | In-place, left-priority merge with property-array merging disabled |
distinctArray(array) |
Function | Remove deeply equal duplicates in place |
isEqual(left, right) |
Function | Compare values recursively, with Date and RegExp handling |
isObject(value) |
Type guard | Non-null object excluding arrays; includes class instances |
isSafeKey(key) |
Function | Reject __proto__, prototype, constructor |
hasOwnProperty(object, key) |
Type guard | Check an own property |
clone(value) |
Function | Use native structuredClone when available, otherwise the fallback |
polyfillClone(value) |
Function | Clone arrays/enumerable string-keyed object fields with cycle tracking |
buildOptions(options?) |
Function | Fill defaults into the supplied options object |
togglePriority(priority) |
Function | Toggle 'left' and 'right' |
PriorityName |
Enum | LEFT, RIGHT |
Options, OptionsInput |
Types | Full options and partial input |
Merger, MergerContext |
Types | Merger call signature and internal context shape |
MergerSource, MergerSourceUnwrap, MergerResult |
Types | Accepted sources and inferred result helpers |
- References: the default creates a new top-level result, but nested objects and arrays can remain shared and can be changed during later merging. Use
clone: truewhen input isolation matters. - Inputs: provide at least one object or array. No sources throws
SyntaxError; this is not a validator for arbitrary input. - Properties: the merge walks enumerable string keys and skips unsafe keys copied from later sources. It does not sanitize an unsafe property already present in an input or clone.
- Cloning: native
structuredClone()can throw for unsupported values such as functions. The fallback does not preserve prototypes or special object semantics. - Equality:
isEqual()and deduplication do not detect cycles. Avoid them on cyclic structures, even though cloning and the merge have cycle bookkeeping.
Made with 💚
Published under the MIT License.