Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow for alternative ways to apply default merging #59

Merged
merged 2 commits into from
Feb 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions docs/deepmergeCustom.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,54 @@ declare module "../src/types" {
}
```

## Default Merging

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:

```ts
const customizedDeepmerge = deepmergeCustom({
mergeOthers: (value, utils) => {
if (someCondition) {
return someCustomValue;
}
return utils.defaultMergeFunctions.mergeOthers(values);
},
});
```

```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
mergeOthers: (value, utils) => {
if (someCondition) {
return someCustomValue;
}
// implicitly return undefined
},
});
```

## API

[See deepmerge custom API](./API.md#deepmergecustomoptions).
189 changes: 170 additions & 19 deletions src/deepmerge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -162,6 +169,8 @@ function getUtils<M, MM extends DeepMergeBuiltInMetaData>(
MM
>["metaDataUpdater"],
deepmerge: customizedDeepmerge,
useImplicitDefaultMerging: options.enableImplicitDefaultMerging ?? false,
actions,
};
}

Expand All @@ -181,11 +190,11 @@ function mergeUnknowns<
return undefined as DeepMergeHKT<Ts, MF, M>;
}
if (values.length === 1) {
return utils.mergeFunctions.mergeOthers(
values,
utils,
meta
) as DeepMergeHKT<Ts, MF, M>;
return mergeOthers<U, M, MM>(values, utils, meta) as DeepMergeHKT<
Ts,
MF,
M
>;
}

const type = getObjectType(values[0]);
Expand All @@ -198,50 +207,192 @@ function mergeUnknowns<
continue;
}

return utils.mergeFunctions.mergeOthers(
values,
utils,
meta
) as DeepMergeHKT<Ts, MF, M>;
return mergeOthers<U, M, MM>(values, utils, meta) as DeepMergeHKT<
Ts,
MF,
M
>;
}
}

switch (type) {
case ObjectType.RECORD:
return utils.mergeFunctions.mergeRecords(
return mergeRecords<U, MF, M, MM>(
values as ReadonlyArray<Readonly<Record<PropertyKey, unknown>>>,
utils,
meta
) as DeepMergeHKT<Ts, MF, M>;

case ObjectType.ARRAY:
return utils.mergeFunctions.mergeArrays(
return mergeArrays<U, M, MM>(
values as ReadonlyArray<Readonly<ReadonlyArray<unknown>>>,
utils,
meta
) as DeepMergeHKT<Ts, MF, M>;

case ObjectType.SET:
return utils.mergeFunctions.mergeSets(
return mergeSets<U, M, MM>(
values as ReadonlyArray<Readonly<ReadonlySet<unknown>>>,
utils,
meta
) as DeepMergeHKT<Ts, MF, M>;

case ObjectType.MAP:
return utils.mergeFunctions.mergeMaps(
return mergeMaps<U, M, MM>(
values as ReadonlyArray<Readonly<ReadonlyMap<unknown, unknown>>>,
utils,
meta
) as DeepMergeHKT<Ts, MF, M>;

default:
return utils.mergeFunctions.mergeOthers(
values,
utils,
meta
) as DeepMergeHKT<Ts, MF, M>;
return mergeOthers<U, M, MM>(values, utils, meta) as DeepMergeHKT<
Ts,
MF,
M
>;
}
}

/**
* Merge records.
*
* @param values - The records.
*/
function mergeRecords<
U extends DeepMergeMergeFunctionUtils<M, MM>,
MF extends DeepMergeMergeFunctionsURIs,
M,
MM extends DeepMergeBuiltInMetaData
>(
values: ReadonlyArray<Readonly<Record<PropertyKey, unknown>>>,
utils: U,
meta: M | undefined
) {
const result = utils.mergeFunctions.mergeRecords(values, utils, meta);

if (
result === actions.defaultMerging ||
(utils.useImplicitDefaultMerging &&
result === undefined &&
utils.mergeFunctions.mergeRecords !==
utils.defaultMergeFunctions.mergeRecords)
) {
return utils.defaultMergeFunctions.mergeRecords<
ReadonlyArray<Readonly<Record<PropertyKey, unknown>>>,
U,
MF,
M,
MM
>(values, utils, meta);
}

return result;
}

/**
* Merge arrays.
*
* @param values - The arrays.
*/
function mergeArrays<
U extends DeepMergeMergeFunctionUtils<M, MM>,
M,
MM extends DeepMergeBuiltInMetaData
>(
values: ReadonlyArray<Readonly<ReadonlyArray<unknown>>>,
utils: U,
meta: M | undefined
) {
const result = utils.mergeFunctions.mergeArrays(values, utils, meta);

if (
result === actions.defaultMerging ||
(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>,
M,
MM extends DeepMergeBuiltInMetaData
>(
values: ReadonlyArray<Readonly<ReadonlySet<unknown>>>,
utils: U,
meta: M | undefined
) {
const result = utils.mergeFunctions.mergeSets(values, utils, meta);

if (
result === actions.defaultMerging ||
(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>,
M,
MM extends DeepMergeBuiltInMetaData
>(
values: ReadonlyArray<Readonly<ReadonlyMap<unknown, unknown>>>,
utils: U,
meta: M | undefined
) {
const result = utils.mergeFunctions.mergeMaps(values, utils, meta);

if (
result === actions.defaultMerging ||
(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>,
M,
MM extends DeepMergeBuiltInMetaData
>(values: ReadonlyArray<unknown>, utils: U, meta: M | undefined) {
const result = utils.mergeFunctions.mergeOthers(values, utils, meta);

if (
result === actions.defaultMerging ||
(utils.useImplicitDefaultMerging &&
result === undefined &&
utils.mergeFunctions.mergeOthers !==
utils.defaultMergeFunctions.mergeOthers)
) {
return utils.defaultMergeFunctions.mergeOthers(values);
}
return result;
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/types/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type DeepMergeOptionsFull<M, MM extends DeepMergeBuiltInMetaData> = Readonly<{
mergeSets: DeepMergeMergeFunctions<M, MM>["mergeSets"] | false;
mergeOthers: DeepMergeMergeFunctions<M, MM>["mergeOthers"];
metaDataUpdater: MetaDataUpdater<M, MM>;
enableImplicitDefaultMerging: boolean;
}>;

/**
Expand Down Expand Up @@ -91,4 +92,8 @@ export type DeepMergeMergeFunctionUtils<
defaultMergeFunctions: DeepMergeMergeFunctionsDefaults;
metaDataUpdater: MetaDataUpdater<M, MM>;
deepmerge: <Ts extends ReadonlyArray<unknown>>(...values: Ts) => unknown;
useImplicitDefaultMerging: boolean;
actions: Readonly<{
defaultMerging: symbol;
}>;
}>;
71 changes: 71 additions & 0 deletions tests/deepmerge-custom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -593,3 +593,74 @@ 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);
});

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);
});