Skip to content

Merge

Combine two (or three) object snapshots with explicit strategies and conflict objects.

Previous: Engines · Next: Query

Import path

ts
import { merge } from "@jayoncode/object-diff/merge";

Not on the root entry — see Engines. Core never imports merge (tree-shake isolation).

Quick example

ts
const result = merge(
  { user: { name: "Ada", role: "admin" } },
  { user: { name: "Grace", role: "admin" } },
  { strategy: "latest-wins" }, // default
);

result.value; // { user: { name: "Grace", role: "admin" } }
result.conflicts; // [{ path: "user.name", left, right, reason: "both-changed" }]
result.applied; // DiffResult of left → value (disable with includeApplied: false)
StrategyBehavior
latest-winsPrefer right on conflict
manualKeep left, list conflicts
customCall resolve(conflict) (required)

Plain objects merge recursively. Without identityKey, arrays and other non-plain values are treated as atomic leaves on conflict.

Identity-aware arrays

Pass the same identityKey contract as diff so list items merge by stable id instead of as one atomic value:

ts
const result = merge(
  {
    items: [
      { id: 1, name: "a" },
      { id: 2, name: "b" },
    ],
  },
  {
    items: [
      { id: 2, name: "b2" },
      { id: 3, name: "c" },
    ],
  },
  { identityKey: "id", strategy: "latest-wins" },
);

// right order first, then left-only ids:
// [{ id: 2, name: "b2" }, { id: 3, name: "c" }, { id: 1, name: "a" }]
// conflicts: [{ path: "items[0].name", reason: "both-changed", ... }]
RuleBehavior
Matched idsDeep-merge the two (or three) item snapshots
Right-only / left-onlyKeep the side that has the item
Result orderItems present on right keep right’s order; surviving left-only ids append in left order
Duplicate idsThrows InvalidOptionsError (same as diff)
Items without an idFall back to index pairing among leftovers

applied (when enabled) also passes identityKey into diff so the audit trail matches list identity.

Conflict objects

Every conflict is listed in conflicts[] (never dropped silently):

FieldMeaning
pathDisplay path of the disagreement
left / rightValues from each side (undefined = missing / deleted)
base?Common ancestor value in three-way merges
reasonboth-changed | delete-edit | both-added
identity?Identity key value when the conflict is under an identity-matched array item (including nested field paths)
ts
merge(left, right, {
  strategy: "custom",
  resolve: (conflict) => {
    if (conflict.reason === "delete-edit") {
      return conflict.right; // or prompt the user
    }
    return conflict.left;
  },
});

Three-way

ts
const result = merge(localDraft, remoteDraft, {
  base: lastSynced,
  strategy: "latest-wins",
  identityKey: "id", // optional — identity-aware lists
});
left vs baseright vs baseResult
equalequalbase
changedequalleft
equalchangedright
changed samechanged samethat value
changed differentlychanged differentlyconflict → strategy

With identityKey, the same table applies per list item. A delete on one side vs an edit on the other raises reason: "delete-edit".

Custom resolve

ts
merge(left, right, {
  strategy: "custom",
  resolve: (conflict) => conflict.left, // or pick UI value
});

Pitfalls

  • Do not import { merge } from "@jayoncode/object-diff" — use /merge.
  • Without identityKey, arrays remain atomic leaves on conflict (not element-wise).
  • custom strategy requires resolve; omitting it is a configuration error.
  • Duplicate identityKey values in any array side throw.

An ecosystem of independent, headless TypeScript libraries engineered for modern web development. Every package includes interactive playgrounds and documentation that evolves alongside the code.