From 1d5e617bc4980f64a75fa9da1397979b2310fc06 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Thu, 24 Feb 2022 02:54:13 +1300 Subject: [PATCH 1/2] feat: allow for implicit default merging --- docs/deepmergeCustom.md | 31 ++++++ src/deepmerge.ts | 174 +++++++++++++++++++++++++++++---- src/types/options.ts | 2 + tests/deepmerge-custom.test.ts | 36 +++++++ 4 files changed, 224 insertions(+), 19 deletions(-) diff --git a/docs/deepmergeCustom.md b/docs/deepmergeCustom.md index a8b70502..5c1e4240 100644 --- a/docs/deepmergeCustom.md +++ b/docs/deepmergeCustom.md @@ -202,6 +202,37 @@ declare module "../src/types" { } ``` +## Implicit Default Merging + +If you do not want to have to explicitly perform default merging in your custom merge functions; +you can set the option `enableImplicitDefaultMerging` to `true`. Once set, if any of your custom +merge functions return `undefined`, then the default merging functions will automatically be called. + +For example, the following `customizedDeepmerge` functions are equivalent: + +```ts +const customizedDeepmerge = deepmergeCustom({ + mergeOthers: (value, utils) => { + if (someCondition) { + return someCustomValue; + } + return utils.defaultMergeFunctions.mergeOthers(values); + }, +}); +``` + +```ts +const customizedDeepmerge = deepmergeCustom({ + enableImplicitDefaultMerging: true, // enable implicit default merging + mergeOthers: (value, utils) => { + if (someCondition) { + return someCustomValue; + } + // implicitly return undefined + }, +}); +``` + ## API [See deepmerge custom API](./API.md#deepmergecustomoptions). diff --git a/src/deepmerge.ts b/src/deepmerge.ts index 16f8b015..c315294f 100644 --- a/src/deepmerge.ts +++ b/src/deepmerge.ts @@ -162,6 +162,7 @@ function getUtils( MM >["metaDataUpdater"], deepmerge: customizedDeepmerge, + useImplicitDefaultMerging: options.enableImplicitDefaultMerging ?? false, }; } @@ -181,11 +182,11 @@ function mergeUnknowns< return undefined as DeepMergeHKT; } if (values.length === 1) { - return utils.mergeFunctions.mergeOthers( - values, - utils, - meta - ) as DeepMergeHKT; + return mergeOthers(values, utils, meta) as DeepMergeHKT< + Ts, + MF, + M + >; } const type = getObjectType(values[0]); @@ -198,50 +199,185 @@ function mergeUnknowns< continue; } - return utils.mergeFunctions.mergeOthers( - values, - utils, - meta - ) as DeepMergeHKT; + return mergeOthers(values, utils, meta) as DeepMergeHKT< + Ts, + MF, + M + >; } } switch (type) { case ObjectType.RECORD: - return utils.mergeFunctions.mergeRecords( + return mergeRecords( values as ReadonlyArray>>, utils, meta ) as DeepMergeHKT; case ObjectType.ARRAY: - return utils.mergeFunctions.mergeArrays( + return mergeArrays( values as ReadonlyArray>>, utils, meta ) as DeepMergeHKT; case ObjectType.SET: - return utils.mergeFunctions.mergeSets( + return mergeSets( values as ReadonlyArray>>, utils, meta ) as DeepMergeHKT; case ObjectType.MAP: - return utils.mergeFunctions.mergeMaps( + return mergeMaps( values as ReadonlyArray>>, utils, meta ) as DeepMergeHKT; default: - return utils.mergeFunctions.mergeOthers( - values, - utils, - meta - ) as DeepMergeHKT; + return mergeOthers(values, utils, meta) as DeepMergeHKT< + Ts, + MF, + M + >; + } +} + +/** + * Merge records. + * + * @param values - The records. + */ +function mergeRecords< + U extends DeepMergeMergeFunctionUtils, + MF extends DeepMergeMergeFunctionsURIs, + M, + MM extends DeepMergeBuiltInMetaData +>( + values: ReadonlyArray>>, + utils: U, + meta: M | undefined +) { + const result = utils.mergeFunctions.mergeRecords(values, utils, meta); + + if ( + utils.useImplicitDefaultMerging && + result === undefined && + utils.mergeFunctions.mergeRecords !== + utils.defaultMergeFunctions.mergeRecords + ) { + return utils.defaultMergeFunctions.mergeRecords< + ReadonlyArray>>, + U, + MF, + M, + MM + >(values, utils, meta); + } + + return result; +} + +/** + * Merge arrays. + * + * @param values - The arrays. + */ +function mergeArrays< + U extends DeepMergeMergeFunctionUtils, + M, + MM extends DeepMergeBuiltInMetaData +>( + values: ReadonlyArray>>, + utils: U, + meta: M | undefined +) { + const result = utils.mergeFunctions.mergeArrays(values, utils, meta); + + if ( + utils.useImplicitDefaultMerging && + result === undefined && + utils.mergeFunctions.mergeArrays !== utils.defaultMergeFunctions.mergeArrays + ) { + return utils.defaultMergeFunctions.mergeArrays(values); + } + return result; +} + +/** + * Merge sets. + * + * @param values - The sets. + */ +function mergeSets< + U extends DeepMergeMergeFunctionUtils, + M, + MM extends DeepMergeBuiltInMetaData +>( + values: ReadonlyArray>>, + utils: U, + meta: M | undefined +) { + const result = utils.mergeFunctions.mergeSets(values, utils, meta); + + if ( + utils.useImplicitDefaultMerging && + result === undefined && + utils.mergeFunctions.mergeSets !== utils.defaultMergeFunctions.mergeSets + ) { + return utils.defaultMergeFunctions.mergeSets(values); + } + return result; +} + +/** + * Merge maps. + * + * @param values - The maps. + */ +function mergeMaps< + U extends DeepMergeMergeFunctionUtils, + M, + MM extends DeepMergeBuiltInMetaData +>( + values: ReadonlyArray>>, + utils: U, + meta: M | undefined +) { + const result = utils.mergeFunctions.mergeMaps(values, utils, meta); + + if ( + utils.useImplicitDefaultMerging && + result === undefined && + utils.mergeFunctions.mergeMaps !== utils.defaultMergeFunctions.mergeMaps + ) { + return utils.defaultMergeFunctions.mergeMaps(values); + } + return result; +} + +/** + * Merge other things. + * + * @param values - The other things. + */ +function mergeOthers< + U extends DeepMergeMergeFunctionUtils, + M, + MM extends DeepMergeBuiltInMetaData +>(values: ReadonlyArray, utils: U, meta: M | undefined) { + const result = utils.mergeFunctions.mergeOthers(values, utils, meta); + + if ( + utils.useImplicitDefaultMerging && + result === undefined && + utils.mergeFunctions.mergeOthers !== utils.defaultMergeFunctions.mergeOthers + ) { + return utils.defaultMergeFunctions.mergeOthers(values); } + return result; } /** diff --git a/src/types/options.ts b/src/types/options.ts index a32b03ad..e4375c54 100644 --- a/src/types/options.ts +++ b/src/types/options.ts @@ -25,6 +25,7 @@ type DeepMergeOptionsFull = Readonly<{ mergeSets: DeepMergeMergeFunctions["mergeSets"] | false; mergeOthers: DeepMergeMergeFunctions["mergeOthers"]; metaDataUpdater: MetaDataUpdater; + enableImplicitDefaultMerging: boolean; }>; /** @@ -91,4 +92,5 @@ export type DeepMergeMergeFunctionUtils< defaultMergeFunctions: DeepMergeMergeFunctionsDefaults; metaDataUpdater: MetaDataUpdater; deepmerge: >(...values: Ts) => unknown; + useImplicitDefaultMerging: boolean; }>; diff --git a/tests/deepmerge-custom.test.ts b/tests/deepmerge-custom.test.ts index 4e9dd9dc..a252428f 100644 --- a/tests/deepmerge-custom.test.ts +++ b/tests/deepmerge-custom.test.ts @@ -593,3 +593,39 @@ test("custom merge that clones", (t) => { t.not(merged.bar, y.bar); t.not(merged.baz, y.baz); }); + +test("implicit default merging", (t) => { + const x = { + foo: 1, + bar: { baz: [2], qux: new Set([1]), quux: new Map([[1, 2]]) }, + }; + const y = { + foo: 3, + bar: { baz: [4], qux: new Set([2]), quux: new Map([[2, 3]]) }, + }; + + const expected = { + foo: 3, + bar: { + baz: [2, 4], + qux: new Set([1, 2]), + quux: new Map([ + [1, 2], + [2, 3], + ]), + }, + }; + + const customizedDeepmerge = deepmergeCustom({ + enableImplicitDefaultMerging: true, + mergeRecords: () => undefined, + mergeArrays: () => undefined, + mergeSets: () => undefined, + mergeMaps: () => undefined, + mergeOthers: () => undefined, + }); + + const merged = customizedDeepmerge(x, y); + + t.deepEqual(merged, expected); +}); From f6ec459988b59fa767ba619b5e2634be6bb3e456 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Thu, 24 Feb 2022 03:16:54 +1300 Subject: [PATCH 2/2] feat: allow for default merging via a special return value --- docs/deepmergeCustom.md | 25 +++++++++++++++--- src/deepmerge.ts | 47 ++++++++++++++++++++++------------ src/types/options.ts | 3 +++ tests/deepmerge-custom.test.ts | 35 +++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 20 deletions(-) diff --git a/docs/deepmergeCustom.md b/docs/deepmergeCustom.md index 5c1e4240..187508a3 100644 --- a/docs/deepmergeCustom.md +++ b/docs/deepmergeCustom.md @@ -202,11 +202,10 @@ declare module "../src/types" { } ``` -## Implicit Default Merging +## Default Merging -If you do not want to have to explicitly perform default merging in your custom merge functions; -you can set the option `enableImplicitDefaultMerging` to `true`. Once set, if any of your custom -merge functions return `undefined`, then the default merging functions will automatically be called. +If you do not want to have to explicitly call the default merging function in your custom merge function; +you can just return `utils.actions.defaultMerging`. This will automatically apply the default merging strategy. For example, the following `customizedDeepmerge` functions are equivalent: @@ -221,6 +220,24 @@ const customizedDeepmerge = deepmergeCustom({ }); ``` +```ts +const customizedDeepmerge = deepmergeCustom({ + mergeOthers: (value, utils) => { + if (someCondition) { + return someCustomValue; + } + return utils.actions.defaultMerging; + }, +}); +``` + +### Implicit + +You can also set the option `enableImplicitDefaultMerging` to `true` to make it so that if any of your +custom merge functions return `undefined`, then the default merging strategy will automatically be applied. + +For example, the following `customizedDeepmerge` function is equivalent to the two above: + ```ts const customizedDeepmerge = deepmergeCustom({ enableImplicitDefaultMerging: true, // enable implicit default merging diff --git a/src/deepmerge.ts b/src/deepmerge.ts index c315294f..4fd4ff85 100644 --- a/src/deepmerge.ts +++ b/src/deepmerge.ts @@ -27,6 +27,13 @@ const defaultMergeFunctions = { mergeOthers: leaf, } as const; +/** + * Special values that tell deepmerge-ts to perform a certain action. + */ +const actions = { + defaultMerging: Symbol("deepmerge-ts: default merging"), +} as const; + /** * The default function to update meta data. */ @@ -163,6 +170,7 @@ function getUtils( >["metaDataUpdater"], deepmerge: customizedDeepmerge, useImplicitDefaultMerging: options.enableImplicitDefaultMerging ?? false, + actions, }; } @@ -263,10 +271,11 @@ function mergeRecords< const result = utils.mergeFunctions.mergeRecords(values, utils, meta); if ( - utils.useImplicitDefaultMerging && - result === undefined && - utils.mergeFunctions.mergeRecords !== - utils.defaultMergeFunctions.mergeRecords + result === actions.defaultMerging || + (utils.useImplicitDefaultMerging && + result === undefined && + utils.mergeFunctions.mergeRecords !== + utils.defaultMergeFunctions.mergeRecords) ) { return utils.defaultMergeFunctions.mergeRecords< ReadonlyArray>>, @@ -297,9 +306,11 @@ function mergeArrays< const result = utils.mergeFunctions.mergeArrays(values, utils, meta); if ( - utils.useImplicitDefaultMerging && - result === undefined && - utils.mergeFunctions.mergeArrays !== utils.defaultMergeFunctions.mergeArrays + result === actions.defaultMerging || + (utils.useImplicitDefaultMerging && + result === undefined && + utils.mergeFunctions.mergeArrays !== + utils.defaultMergeFunctions.mergeArrays) ) { return utils.defaultMergeFunctions.mergeArrays(values); } @@ -323,9 +334,10 @@ function mergeSets< const result = utils.mergeFunctions.mergeSets(values, utils, meta); if ( - utils.useImplicitDefaultMerging && - result === undefined && - utils.mergeFunctions.mergeSets !== utils.defaultMergeFunctions.mergeSets + result === actions.defaultMerging || + (utils.useImplicitDefaultMerging && + result === undefined && + utils.mergeFunctions.mergeSets !== utils.defaultMergeFunctions.mergeSets) ) { return utils.defaultMergeFunctions.mergeSets(values); } @@ -349,9 +361,10 @@ function mergeMaps< const result = utils.mergeFunctions.mergeMaps(values, utils, meta); if ( - utils.useImplicitDefaultMerging && - result === undefined && - utils.mergeFunctions.mergeMaps !== utils.defaultMergeFunctions.mergeMaps + result === actions.defaultMerging || + (utils.useImplicitDefaultMerging && + result === undefined && + utils.mergeFunctions.mergeMaps !== utils.defaultMergeFunctions.mergeMaps) ) { return utils.defaultMergeFunctions.mergeMaps(values); } @@ -371,9 +384,11 @@ function mergeOthers< const result = utils.mergeFunctions.mergeOthers(values, utils, meta); if ( - utils.useImplicitDefaultMerging && - result === undefined && - utils.mergeFunctions.mergeOthers !== utils.defaultMergeFunctions.mergeOthers + result === actions.defaultMerging || + (utils.useImplicitDefaultMerging && + result === undefined && + utils.mergeFunctions.mergeOthers !== + utils.defaultMergeFunctions.mergeOthers) ) { return utils.defaultMergeFunctions.mergeOthers(values); } diff --git a/src/types/options.ts b/src/types/options.ts index e4375c54..3d982665 100644 --- a/src/types/options.ts +++ b/src/types/options.ts @@ -93,4 +93,7 @@ export type DeepMergeMergeFunctionUtils< metaDataUpdater: MetaDataUpdater; deepmerge: >(...values: Ts) => unknown; useImplicitDefaultMerging: boolean; + actions: Readonly<{ + defaultMerging: symbol; + }>; }>; diff --git a/tests/deepmerge-custom.test.ts b/tests/deepmerge-custom.test.ts index a252428f..210cfe8c 100644 --- a/tests/deepmerge-custom.test.ts +++ b/tests/deepmerge-custom.test.ts @@ -629,3 +629,38 @@ test("implicit default merging", (t) => { t.deepEqual(merged, expected); }); + +test("default merging using shortcut", (t) => { + const x = { + foo: 1, + bar: { baz: [2], qux: new Set([1]), quux: new Map([[1, 2]]) }, + }; + const y = { + foo: 3, + bar: { baz: [4], qux: new Set([2]), quux: new Map([[2, 3]]) }, + }; + + const expected = { + foo: 3, + bar: { + baz: [2, 4], + qux: new Set([1, 2]), + quux: new Map([ + [1, 2], + [2, 3], + ]), + }, + }; + + const customizedDeepmerge = deepmergeCustom({ + mergeRecords: (value, utils) => utils.actions.defaultMerging, + mergeArrays: (value, utils) => utils.actions.defaultMerging, + mergeSets: (value, utils) => utils.actions.defaultMerging, + mergeMaps: (value, utils) => utils.actions.defaultMerging, + mergeOthers: (value, utils) => utils.actions.defaultMerging, + }); + + const merged = customizedDeepmerge(x, y); + + t.deepEqual(merged, expected); +});