Skip to content
tada5hiPublic

About

A zero dependency library to safe merge objects and arrays with customizable behavior.

Topics

Resources

Stars

64 stars

Watchers

1 watching

Forks

Repository files navigation

smob logo

smob

Zero dependency library to safe merge objects.

npm version CI status License

Why smob? · Install · Usage · Options · API


Why smob?

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'] }

merge combines the left source containing port 3000, db.host local and tags app with the right source containing port 80, db.pool 5 and tags defaults. The result keeps port 3000, combines db properties and concatenates tags in left-to-right order.

Table of Contents

Installation

npm install smob

Requires Node.js 20 or newer. ESM, CommonJS and TypeScript declarations are included. There are no runtime dependencies.

Usage

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']

Merger

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 }

Set precedence and array behavior

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().

Handle a conflict yourself

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.

Assign defaults and deduplicate

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'] })); // true

assign() mutates the target, keeps its existing values and disables merging array-valued properties. Its precedence differs from Object.assign(). distinctArray() also mutates its input.

Options

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

Utils

distinctArray

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']]

isEqual

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'])); // true

Avoid deep equality and deduplication on cyclic structures; these helpers have no cycle detection.

API

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

Notes

  • 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: true when 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.

License

Made with 💚

Published under the MIT License.

About

A zero dependency library to safe merge objects and arrays with customizable behavior.

Topics

Resources

Stars

64 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages