deep-merge-many
v2.0.1
Published
Deep-merge many plain objects in one call — fast at scale, min/max on numeric leaves.
Downloads
753
Maintainers
Readme
deep-merge-many
Deep-merge many plain objects in one call. Recurses into nested objects; combines numeric leaves with Math.max, except keys named min which use Math.min.
Optimized for merging large batches (10+ objects) in a single pass — see benchmarks.
Try it: Interactive demo — edit several JSON objects and see the merged result live.
Why this exists
deep-merge-many started as in-app merge logic for a product that filters search results by per-user, per-item eligibility on our side. We could not express that in a single Algolia query, so we ran many searches in parallel (different filter sets / curated collections), applied eligibility to the hits, and then had to present one facet panel for the combined result set.
Each Algolia search response includes:
facets— facet value counts ({ [facetName]: { [value]: number } })facets_stats— min / max (and related stats) for numeric facets ({ [facetName]: FacetStats })
After merging hits from several responses, the UI still needs a union of that metadata across every query that contributed results:
| Field | Goal when combining responses |
| --- | --- |
| facets | For each facet value, keep the highest count seen in any response (a value visible in any query should count). |
| facets_stats | Widen numeric ranges: max of each max, min of each min (and the same max rule for other numeric stat fields). |
deepMergeMany encodes exactly that: numeric leaves use Math.max, except keys named min which use Math.min. Nested objects (facet names, then values or stat keys) are merged recursively. The helper was extracted into this small, dependency-free package so the same semantics are reusable anywhere you merge many plain objects at scale—not only Algolia.
flowchart LR
eligibility[Per-user item eligibility]
filters[Many Algolia filter sets]
queries[Parallel search queries]
meta["facets + facets_stats per response"]
merge[deepMergeMany]
ui[Unified facet UI]
eligibility --> filters --> queries --> meta --> merge --> uiBehavior
- Nested plain objects are merged recursively (arrays and other types are leaves).
- Numeric leaves use
Math.maxby default. - Keys named
minuseMath.mininstead. - Empty entries,
undefined, and{}are skipped when collecting keys.
Install
npm install deep-merge-manyUsage
import { deepMergeMany } from "deep-merge-many";
const merged = deepMergeMany([
{ bounds: { price: { min: 10, max: 100 } }, counts: { a: 3, b: 1 } },
{ bounds: { price: { min: 5, max: 200 } }, counts: { a: 1, b: 5 } },
// …more pages or chunks
]);
// {
// bounds: { price: { min: 5, max: 200 } },
// counts: { a: 3, b: 5 },
// }Algolia facets and facets_stats
After parallel queries, merge the facet metadata from each response (omit undefined / empty objects if a query returned no facets):
import { deepMergeMany } from "deep-merge-many";
import type { BaseSearchResponse } from "algoliasearch/lite";
const responses: BaseSearchResponse[] = /* parallel search results */;
const facets = deepMergeMany(
responses.map((r) => r.facets).filter(Boolean),
) as NonNullable<BaseSearchResponse["facets"]>;
const facets_stats = deepMergeMany(
responses.map((r) => r.facets_stats).filter(Boolean),
) as NonNullable<BaseSearchResponse["facets_stats"]>;Example: two responses for the same brand facet — counts take the max per value; stats widen min/max across queries:
deepMergeMany([
{ brand: { Nike: 10, Adidas: 3 }, price: { min: 20, max: 100, avg: 50 } },
{ brand: { Nike: 4, Puma: 7 }, price: { min: 5, max: 200, avg: 80 } },
]);
// {
// brand: { Nike: 10, Adidas: 3, Puma: 7 },
// price: { min: 5, max: 200, avg: 80 },
// }The export is deepMergeMany — one function, any number of input objects.
Development
Requires pnpm 11+ and Node 18+.
pnpm install
pnpm test
pnpm run buildSee CONTRIBUTING.md for pull requests and PUBLISHING.md for npm releases.
Benchmarks
Multi-object merge throughput (5 → 100 nested objects) vs @fastify/deepmerge, ts-deepmerge, deepmerge, and deep-merge.
deep-merge-many leads from ~10 objects upward on identical payloads. Other libraries use different merge rules — this measures throughput, not identical output.

Bundle weight (each library’s merge entry point, esbuild-bundled for the browser, minified, gzip level 9):

pnpm bench
open benchmark/chart.htmlRegenerates benchmark/chart.html, docs/benchmark.svg, docs/benchmark.png, and docs/benchmark-size.svg / .png.
License
MIT
