deepmerge-ts

Type-safe deep merging for TypeScript objects, arrays, Maps, and Sets with a smart, high-performance merge strategy.

Library
npm
v8.0.2
303stars
BSD 3-Clause License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
78/100Good
Development Activity96
Maintenance96
Community36
Maturity56
Momentum28

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
90/100Excellent
Architecture88
Code Quality95
Innovation78
Learning Curve100

deepmerge-ts deeply merges two or more JavaScript objects while preserving accurate TypeScript types for the merged result, so you don’t lose type information or need to cast after a merge. It handles records, arrays, Maps, Sets, and circular references out of the box, and ships a fully customizable deepmergeCustom API for overriding how any of those types get combined.

Instead of the classic pairwise merge-then-merge-again strategy used by many alternatives, it merges all inputs in one smart pass, which improves both execution time and memory usage as the number of merged objects grows. For performance-critical code operating on trusted, non-circular data, deepmergeFastUnsafe variants skip circular-reference tracking, depth limits, and prototype-pollution safeguards for extra speed.

What You Get

  • Type-safe merge functions (deepmerge, deepmergeCustom, deepmergeInto, deepmergeIntoCustom) whose return types are inferred directly from the inputs.
  • Built-in merging support for plain records, arrays, Map, and Set, plus safe handling of circular references.
  • A customizer API for overriding how records, arrays, maps, sets, circular references, or any other value type get merged.
  • High-performance deepmergeFastUnsafe / deepmergeIntoFastUnsafe variants that skip safety checks for trusted, non-circular data.
  • A configurable recursion depth limit (default 1000) and prototype-pollution safeguards for merging untrusted input safely.

Common Use Cases

  • Merging default configuration objects with user-supplied overrides while keeping full type inference.
  • Combining partial state updates (including nested arrays/Maps/Sets) in state-management or reducer logic.
  • Building a deep-merge with custom array/record strategies (e.g. concatenation instead of overwrite) via deepmergeCustom.
  • Merging multiple layered config sources (defaults, environment, CLI flags) safely, with depth limits guarding against malicious/untrusted input.

Under The Hood

Architecture The core mergeUnknowns function in src/deepmerge.ts dispatches by object type (record, array, set, map, other) into type-specific merge functions, each of which first calls a possibly-user-overridden function from utils.mergeFunctions and falls back to utils.defaultMergeFunctions when the custom function signals utils.actions.defaultMerge (implemented via shouldFallbackToDefault in defaults/general.ts). The four public variants — deepmerge, deepmergeFastUnsafe, deepmergeInto, deepmergeIntoFastUnsafe — each get their own defaults module (defaults/vanilla.ts, vanilla-fast.ts, into.ts, into-fast.ts) implementing the same DeepMergeUtils/DeepMergeIntoUtils contract, so the dispatch core is shared while behavior only diverges in the default merge functions and how utils gets constructed. This is a clean strategy-pattern layout: swapping defaultMergeFunctions is essentially the only thing that changes between merge modes.

Tech Stack The library has zero runtime dependencies. It’s written in strict TypeScript targeting ESNext, built into a dual CJS/ESM package via Rollup with a dedicated TypeScript plugin and bundled declaration generation, and published to both npm and JSR from the same source. The repo uses a pnpm workspace with a separate benchmark package, splits type-checking into root and src tsconfigs (both strict: true), and tests with Vitest plus tsd for compile-time type-assertion tests. Linting layers ESLint (flat config, extensive plugin set), markdownlint, cspell, knip, and publint, with releases fully automated through semantic-release and Conventional-Commit enforcement via Husky and commitlint.

Code Quality Test coverage is extensive — thousands of lines across dedicated test files for each merge variant, plus .test-d.ts files asserting the exact inferred type of merge results via tsd, not just runtime behavior. CI runs separate workflows for JS tests with coverage, type tests, general typechecking, and multiple lint passes, backed by pre-commit hooks. Naming conventions flag mutation explicitly (a mut_ prefix on mutable parameters like mut_target), and the library favors returning depth-limited or cyclic-safe results over throwing, relying on TypeScript’s type system and explicit guards rather than runtime exceptions for correctness.

What Makes It Unique Its core differentiator against classic deep-merge libraries is twofold: type-accurate merging, where the return type of a merge call is inferred to reflect exactly what was combined, and a “smart merge” strategy that looks across all inputs at once instead of reducing them pairwise — improving performance on large input counts and enabling behavior a pairwise reduce can’t easily express. Paired with distinct immutable/in-place APIs and separate audited-vs-unaudited code paths for the same operations, this is a deliberately differentiated design within a well-trodden problem space.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search