From b8dd88a60093c2cc65af2fa59153c856cd529e76 Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Thu, 24 Feb 2022 22:21:15 +1300 Subject: [PATCH] feat: allow for alternative ways to apply default merging --- docs/deepmergeCustom.md | 45 +++++++++++++++++++++++++++++----- src/deepmerge.ts | 31 ++++++++++++----------- src/types/options.ts | 9 ++++--- tests/deepmerge-custom.test.ts | 37 +++++++++++++++++++++++----- 4 files changed, 92 insertions(+), 30 deletions(-) diff --git a/docs/deepmergeCustom.md b/docs/deepmergeCustom.md index 97da3077..96811024 100644 --- a/docs/deepmergeCustom.md +++ b/docs/deepmergeCustom.md @@ -202,10 +202,41 @@ declare module "../src/types" { } ``` -## Default Merging +## Special Actions + +We provide a couple of special actions under `utils.actions` that you can use to simplify your custom merge functions. + +### Skipping a Property (`utils.actions.skip`) + +If you want to skip a property from being included in the result of a merge based on its value or metadata, you can easily do so with this action. + +For example, skipping all properties under the key `"skipme"` of type `Date`: + +```ts +const customizedDeepmerge = deepmergeCustom({ + mergeOthers: (value, utils, meta) => { + if (meta?.key === "skipme") { + const nonDateValues = values.filter((value) => !(value instanceof Date)); + if (nonDateValues.length === 0) { + return utils.actions.skip; // Completely skip this property + } + + // Don't skip the property completely if a non-Date value was found. + return utils.defaultMergeFunctions.mergeOthers(nonDateValues); + } + + // Perform the default merging (see below). + return utils.actions.defaultMerge; + }, +}); +``` + +To do this without any special actions would require using a custom `mergeRecords` which would be a bit more complicated. + +### Default Merging (`utils.actions.defaultMerge`) If you do not want to have to explicitly call the default merging function in your custom merge function; -you can just return `utils.use.defaultMerging`. This will automatically apply the default merging strategy. +you can just return `utils.actions.defaultMerge`. This will automatically apply the default merging strategy. For example, the following `customizedDeepmerge` functions are equivalent: @@ -226,21 +257,23 @@ const customizedDeepmerge = deepmergeCustom({ if (someCondition) { return someCustomValue; } - return utils.use.defaultMerging; + return utils.actions.defaultMerge; }, }); ``` -### Implicit +Note: When using this action, you cannot change the values upon which the default merging will apply. + +#### Implicit Default Merging -You can also set the option `allowImplicitDefaultMerging` to `true` to make it so that if any of your +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({ - allowImplicitDefaultMerging: true, // enable implicit default merging + enableImplicitDefaultMerging: true, // enable implicit default merging mergeOthers: (value, utils) => { if (someCondition) { return someCustomValue; diff --git a/src/deepmerge.ts b/src/deepmerge.ts index c3533e98..49775b10 100644 --- a/src/deepmerge.ts +++ b/src/deepmerge.ts @@ -27,8 +27,11 @@ const defaultMergeFunctions = { mergeOthers: leaf, } as const; -const shortcutActions = { - defaultMerging: Symbol("deepmerge-ts: default merging"), +/** + * Special values that tell deepmerge-ts to perform a certain action. + */ +const actions = { + defaultMerge: Symbol("deepmerge-ts: default merge"), } as const; /** @@ -166,8 +169,8 @@ function getUtils( MM >["metaDataUpdater"], deepmerge: customizedDeepmerge, - allowImplicitDefaultMerging: options.allowImplicitDefaultMerging ?? false, - use: shortcutActions, + useImplicitDefaultMerging: options.enableImplicitDefaultMerging ?? false, + actions, }; } @@ -268,8 +271,8 @@ function mergeRecords< const result = utils.mergeFunctions.mergeRecords(values, utils, meta); if ( - result === shortcutActions.defaultMerging || - (utils.allowImplicitDefaultMerging && + result === actions.defaultMerge || + (utils.useImplicitDefaultMerging && result === undefined && utils.mergeFunctions.mergeRecords !== utils.defaultMergeFunctions.mergeRecords) @@ -303,8 +306,8 @@ function mergeArrays< const result = utils.mergeFunctions.mergeArrays(values, utils, meta); if ( - result === shortcutActions.defaultMerging || - (utils.allowImplicitDefaultMerging && + result === actions.defaultMerge || + (utils.useImplicitDefaultMerging && result === undefined && utils.mergeFunctions.mergeArrays !== utils.defaultMergeFunctions.mergeArrays) @@ -331,8 +334,8 @@ function mergeSets< const result = utils.mergeFunctions.mergeSets(values, utils, meta); if ( - result === shortcutActions.defaultMerging || - (utils.allowImplicitDefaultMerging && + result === actions.defaultMerge || + (utils.useImplicitDefaultMerging && result === undefined && utils.mergeFunctions.mergeSets !== utils.defaultMergeFunctions.mergeSets) ) { @@ -358,8 +361,8 @@ function mergeMaps< const result = utils.mergeFunctions.mergeMaps(values, utils, meta); if ( - result === shortcutActions.defaultMerging || - (utils.allowImplicitDefaultMerging && + result === actions.defaultMerge || + (utils.useImplicitDefaultMerging && result === undefined && utils.mergeFunctions.mergeMaps !== utils.defaultMergeFunctions.mergeMaps) ) { @@ -381,8 +384,8 @@ function mergeOthers< const result = utils.mergeFunctions.mergeOthers(values, utils, meta); if ( - result === shortcutActions.defaultMerging || - (utils.allowImplicitDefaultMerging && + result === actions.defaultMerge || + (utils.useImplicitDefaultMerging && result === undefined && utils.mergeFunctions.mergeOthers !== utils.defaultMergeFunctions.mergeOthers) diff --git a/src/types/options.ts b/src/types/options.ts index 84b7519e..9cb8e447 100644 --- a/src/types/options.ts +++ b/src/types/options.ts @@ -25,7 +25,7 @@ type DeepMergeOptionsFull = Readonly<{ mergeSets: DeepMergeMergeFunctions["mergeSets"] | false; mergeOthers: DeepMergeMergeFunctions["mergeOthers"]; metaDataUpdater: MetaDataUpdater; - allowImplicitDefaultMerging: boolean; + enableImplicitDefaultMerging: boolean; }>; /** @@ -92,8 +92,9 @@ export type DeepMergeMergeFunctionUtils< defaultMergeFunctions: DeepMergeMergeFunctionsDefaults; metaDataUpdater: MetaDataUpdater; deepmerge: >(...values: Ts) => unknown; - allowImplicitDefaultMerging: boolean; - use: Readonly<{ - defaultMerging: symbol; + useImplicitDefaultMerging: boolean; + actions: Readonly<{ + defaultMerge: symbol; + skip: symbol; }>; }>; diff --git a/tests/deepmerge-custom.test.ts b/tests/deepmerge-custom.test.ts index 04e73a04..fa27a200 100644 --- a/tests/deepmerge-custom.test.ts +++ b/tests/deepmerge-custom.test.ts @@ -617,7 +617,7 @@ test("implicit default merging", (t) => { }; const customizedDeepmerge = deepmergeCustom({ - allowImplicitDefaultMerging: true, + enableImplicitDefaultMerging: true, mergeRecords: () => undefined, mergeArrays: () => undefined, mergeSets: () => undefined, @@ -653,11 +653,36 @@ test("default merging using shortcut", (t) => { }; const customizedDeepmerge = deepmergeCustom({ - mergeRecords: (value, utils) => utils.use.defaultMerging, - mergeArrays: (value, utils) => utils.use.defaultMerging, - mergeSets: (value, utils) => utils.use.defaultMerging, - mergeMaps: (value, utils) => utils.use.defaultMerging, - mergeOthers: (value, utils) => utils.use.defaultMerging, + mergeRecords: (value, utils) => utils.actions.defaultMerge, + mergeArrays: (value, utils) => utils.actions.defaultMerge, + mergeSets: (value, utils) => utils.actions.defaultMerge, + mergeMaps: (value, utils) => utils.actions.defaultMerge, + mergeOthers: (value, utils) => utils.actions.defaultMerge, + }); + + const merged = customizedDeepmerge(x, y); + + t.deepEqual(merged, expected); +}); + +test("skip property", (t) => { + const x = { + foo: { bar: 1, baz: 2, qux: ["a"] }, + bar: [1, 2, 3], + }; + const y = { + foo: { bar: 3, baz: 4, qux: ["b"] }, + bar: [4, 5, 6], + }; + + const expected = { + foo: { baz: 4, qux: ["a", "b"] }, + bar: [1, 2, 3, 4, 5, 6], + }; + + const customizedDeepmerge = deepmergeCustom({ + mergeOthers: (value, utils, meta) => + meta?.key === "bar" ? utils.actions.skip : utils.actions.defaultMerge, }); const merged = customizedDeepmerge(x, y);