deepMerge function
Applies a partial patch to nested settings, such as saved preferences over the defaults: plain objects are merged at every depth, other values replace, undefined keeps. A __proto__ key stays a plain property.
ts
import { deepMerge } from 'utils';Signature
ts
export function deepMerge<T extends object>(base: T, patch: DeepPartial<NoInfer<T>>): TParameters
| Name | Description |
|---|---|
T | The type of the settings. |
base | The complete object, such as the defaults. |
patch | The values to change. |
Returns
A new object that shares the unchanged branches with base.
Example
ts
deepMerge({ grid: { step: 10, isVisible: true } }, { grid: { isVisible: false } });
// { grid: { step: 10, isVisible: false } }Tests
Line coverage: 100 %.
- ✅ merges nested objects and replaces other values
- ✅ keeps the values that the patch leaves undefined
- ✅ shares unchanged branches and modifies no argument
- ✅ adds keys missing from the base
- ✅ keeps proto from JSON as an ordinary property
Source
src/object/deep-merge.ts (on GitHub)
Uses
isRecord: Checks whether a value is a plain object: an object literal orObject.create(null).DeepPartial: Makes every property optional, at every depth: a patch of a nested settings object, read-only since a patch is only read.
Files it needs
src/object/deep-merge.ts imports, directly or not:
Source code
ts
import { isRecord } from '../guard';
import type { DeepPartial } from '../types';
function mergeValues(base: unknown, patch: unknown): unknown {
if (patch === undefined) {
return base;
}
if (!isRecord(base) || !isRecord(patch)) {
return patch;
}
return Object.fromEntries([
...Object.entries(base),
...Object.entries(patch).map(([key, value]) => [
key,
mergeValues(Object.hasOwn(base, key) ? base[key] : undefined, value),
]),
]);
}
/**
* Applies a partial patch to nested settings, such as saved preferences over the defaults: plain objects are
* merged at every depth, other values replace, `undefined` keeps. A `__proto__` key stays a plain property.
*
* @template T - The type of the settings.
* @param base - The complete object, such as the defaults.
* @param patch - The values to change.
* @returns A new object that shares the unchanged branches with `base`.
* @example
* deepMerge({ grid: { step: 10, isVisible: true } }, { grid: { isVisible: false } });
* // { grid: { step: 10, isVisible: false } }
*/
export function deepMerge<T extends object>(base: T, patch: DeepPartial<NoInfer<T>>): T {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- A DeepPartial<T> merged into a T is a T.
return mergeValues(base, patch) as T;
}Test file
ts
import { assert, isRecord } from '../guard';
import { deepMerge } from './deep-merge';
describe(deepMerge, () => {
const defaults = { grid: { step: 10, isVisible: true }, series: ['a', 'b'], title: 'Speed' };
it('merges nested objects and replaces other values', () => {
expect(deepMerge(defaults, { grid: { isVisible: false }, series: ['c'] })).toStrictEqual({
grid: { step: 10, isVisible: false },
series: ['c'],
title: 'Speed',
});
});
it('keeps the values that the patch leaves undefined', () => {
const base: { title?: string | undefined; grid?: { step: number } | undefined } = { title: 'a', grid: { step: 1 } };
expect(deepMerge(base, { title: undefined, grid: undefined })).toStrictEqual(base);
});
it('shares unchanged branches and modifies no argument', () => {
const patch = { title: 'Heading' };
const result = deepMerge(defaults, patch);
expect(result.grid).toBe(defaults.grid);
expect(defaults.title).toBe('Speed');
expect(patch).toStrictEqual({ title: 'Heading' });
});
it('adds keys missing from the base', () => {
expect(deepMerge<{ a?: { b?: number } }>({}, { a: { b: 1 } })).toStrictEqual({ a: { b: 1 } });
});
it('keeps __proto__ from JSON as an ordinary property', () => {
const patch: unknown = JSON.parse('{ "__proto__": { "isAdmin": true } }');
assert(isRecord(patch), 'Assertion failed');
const result = deepMerge<Record<string, unknown>>({}, patch);
expect(Object.getPrototypeOf(result)).toBe(Object.prototype);
expect(Object.hasOwn(result, '__proto__')).toBe(true);
expect(Object.prototype).not.toHaveProperty('isAdmin');
});
});Keywords: deep, merge, objects, function