Skip to content

Commit

Permalink
feat: provide customizable meta data to custom merge functions
Browse files Browse the repository at this point in the history
fix #33
  • Loading branch information
RebeccaStevens committed Feb 19, 2022
1 parent d490307 commit 3d96692
Show file tree
Hide file tree
Showing 11 changed files with 682 additions and 135 deletions.
18 changes: 12 additions & 6 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Merges the array of inputs together using the default configuration.

Note: If `inputs` isn't typed as a tuple then we cannot determine the output type. The output type will simply be `unknown`.

## deepmergeCustom(options)
## deepmergeCustom(options[, rootMetaData])

Generate a customized deepmerge function using the given options. The returned function works just like `deepmerge` except it uses the customized configuration.

Expand All @@ -21,38 +21,44 @@ All these options are optional.

#### `mergeRecords`

Type: `false | (values: Record<any, unknown>[], utils: DeepMergeMergeFunctionUtils) => unknown`
Type: `false | (values: Record<any, unknown>[], utils: DeepMergeMergeFunctionUtils, meta: MetaData) => unknown`

If false, records won't be merged. If set to a function, that function will be used to merge records.

Note: Records are "vanilla" objects (e.g. `{ foo: "hello", bar: "world" }`).

#### `mergeArrays`

Type: `false | (values: unknown[][], utils: DeepMergeMergeFunctionUtils) => unknown`
Type: `false | (values: unknown[][], utils: DeepMergeMergeFunctionUtils, meta: MetaData) => unknown`

If false, arrays won't be merged. If set to a function, that function will be used to merge arrays.

#### `mergeMaps`

Type: `false | (values: Map<unknown, unknown>[], utils: DeepMergeMergeFunctionUtils) => unknown`
Type: `false | (values: Map<unknown, unknown>[], utils: DeepMergeMergeFunctionUtils, meta: MetaData) => unknown`

If false, maps won't be merged. If set to a function, that function will be used to merge maps.

#### `mergeSets`

Type: `false | (values: Set<unknown>[], utils: DeepMergeMergeFunctionUtils) => unknown`
Type: `false | (values: Set<unknown>[], utils: DeepMergeMergeFunctionUtils, meta: MetaData) => unknown`

If false, sets won't be merged. If set to a function, that function will be used to merge sets.

#### `mergeOthers`

Type: `(values: unknown[], utils: DeepMergeMergeFunctionUtils) => unknown`
Type: `(values: unknown[], utils: DeepMergeMergeFunctionUtils, meta: MetaData) => unknown`

If set to a function, that function will be used to merge everything else.

Note: This includes merging mixed types, such as merging a map with an array.

### `rootMetaData`

Type: `MetaData`

The given meta data value will be passed to root level merges.

### DeepMergeMergeFunctionUtils

This is a set of utility functions that are made available to your custom merge functions.
Expand Down
121 changes: 117 additions & 4 deletions docs/deepmergeCustom.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ This can be done using [Declaration Merging](https://www.typescriptlang.org/docs

```ts
declare module "deepmerge-ts" {
interface DeepMergeMergeFunctionURItoKind<Ts extends ReadonlyArray<unknown>, MF extends DeepMergeMergeFunctionsURIs> {
interface DeepMergeMergeFunctionURItoKind<Ts extends ReadonlyArray<unknown>, MF extends DeepMergeMergeFunctionsURIs, M> {
readonly MyCustomMergeURI: MyValue;
}
}
Expand All @@ -52,13 +52,13 @@ import { deepmergeCustom } from "deepmerge-ts";
const customizedDeepmerge = deepmergeCustom<{
DeepMergeOthersURI: "MyDeepMergeDatesURI"; // <-- Needed for correct output type.
}>({
mergeOthers: (values, utils) => {
mergeOthers: (values, utils, meta) => {
// If every value is a date, the return the amalgamated array.
if (values.every((value) => value instanceof Date)) {
return values;
}
// Otherwise, use the default merging strategy.
return utils.defaultMergeFunctions.mergeOthers(values, utils);
return utils.defaultMergeFunctions.mergeOthers(values);
},
});

Expand All @@ -71,7 +71,8 @@ customDeepmerge(x, y, z); // => { foo: [Date, Date, Date] }
declare module "deepmerge-ts" {
interface DeepMergeMergeFunctionURItoKind<
Ts extends ReadonlyArray<unknown>,
MF extends DeepMergeMergeFunctionsURIs
MF extends DeepMergeMergeFunctionsURIs,
M
> {
readonly MyDeepMergeDatesURI: EveryIsDate<Ts> extends true ? Ts : DeepMergeLeaf<Ts>;
}
Expand All @@ -86,6 +87,118 @@ type EveryIsDate<Ts extends ReadonlyArray<unknown>> = Ts extends readonly [infer

Note: If you want to use HKTs in your own project, not related to deepmerge-ts, we recommend checking out [fp-ts](https://gcanti.github.io/fp-ts/modules/HKT.ts.html).

## Meta Data

We provide a simple object of meta data that states the key that the values being merged were under.

Here's an example that creates a custom deepmerge function that merges numbers differently based on the key they were under.

```ts
import type { DeepMergeLeaf, DeepMergeMergeFunctionURItoKind, DeepMergeMergeFunctionsURIs } from "deepmerge-ts";
import { deepmergeCustom } from "deepmerge-ts";

const customizedDeepmerge = deepmergeCustom({
mergeOthers: (values, utils, meta) => {
if (meta !== undefined && areAllNumbers(values)) {
const { key } = meta;
const numbers: ReadonlyArray<number> = values;

if (key === "sum") {
return numbers.reduce((sum, value) => sum + value);
}
if (key === "product") {
return numbers.reduce((prod, value) => prod * value);
}
if (key === "mean") {
return numbers.reduce((sum, value) => sum + value) / numbers.length;
}
}

return utils.defaultMergeFunctions.mergeOthers(values);
},
});

function areAllNumbers(values: ReadonlyArray<unknown>): values is ReadonlyArray<number> {
return values.every((value) => typeof value === "number");
}

const v = { sum: 1, product: 2, mean: 3 };
const x = { sum: 4, product: 5, mean: 6 };
const y = { sum: 7, product: 8, mean: 9 };
const z = { sum: 10, product: 11, mean: 12 };

customizedDeepmerge(v, x, y, z); // => { sum: 22, product: 880, mean: 7.5 }
```

### Customizing the Meta Data

You can customize the meta data that is passed to the merge functions by providing a `metaDataUpdater` function.

Here's an example that uses custom metadata that accumulates the full key path.

```ts
import type { DeepMergeLeaf, DeepMergeMergeFunctionURItoKind, DeepMergeMergeFunctionsURIs } from "deepmerge-ts";
import { deepmergeCustom } from "deepmerge-ts";

const customizedDeepmerge = deepmergeCustom<
// Change the return type of `mergeOthers`.
{
DeepMergeOthersURI: "KeyPathBasedMerge";
},
// Change the meta data type.
{
keyPath: ReadonlyArray<PropertyKey>;
}
>({
// Customize what the actual meta data.
metaDataUpdater: (previousMeta, metaMeta) => {
if (previousMeta === undefined) {
return { keyPath: [] };
}
return {
...metaMeta,
keyPath: [...previousMeta.keyPath, metaMeta.key],
};
},
// Use the meta data when merging others.
mergeOthers: (values, utils, meta) => {
if (
meta !== undefined &&
meta.keyPath.length >= 2 &&
meta.keyPath[meta.keyPath.length - 2] === "bar" &&
meta.keyPath[meta.keyPath.length - 1] === "baz"
) {
return "special merge";
}

return utils.defaultMergeFunctions.mergeOthers(values);
},
});

const x = {
foo: { bar: { baz: 1, qux: 2 } },
bar: { baz: 3, qux: 4 },
};
const y = {
foo: { bar: { baz: 5, bar: { baz: 6, qux: 7 } } },
bar: { baz: 8, qux: 9 },
};

customizedDeepmerge(x, y); // => { foo: { bar: { baz: "special merge", bar: { baz: 6, qux: 7 }, qux: 2 } }, bar: { baz: "special merge", qux: 9 }, }

declare module "../src/types" {
interface DeepMergeMergeFunctionURItoKind<
Ts extends Readonly<ReadonlyArray<unknown>>,
MF extends DeepMergeMergeFunctionsURIs,
M // This is the meta data type
> {
readonly KeyPathBasedMerge: Ts[number] extends number
? Ts[number] | string
: DeepMergeLeaf<Ts>;
}
}
```

## API

[See deepmerge custom API](./API.md#deepmergecustomoptions).

0 comments on commit 3d96692

Please sign in to comment.