Skip to content

Commit

Permalink
feat: create deepmergeInto function
Browse files Browse the repository at this point in the history
fix #51
  • Loading branch information
RebeccaStevens committed Feb 6, 2023
1 parent 3fd0c2f commit 9c350a0
Show file tree
Hide file tree
Showing 22 changed files with 2,299 additions and 220 deletions.
35 changes: 18 additions & 17 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,33 +36,34 @@
"/[A-Za-z0-9]{32,}/"
],
"words": [
"bar",
"baz",
"builtins",
"codesandbox",
"corge",
"customizer",
"deepmerge",
"deepmergecustomoptions",
"deepmergets",
"denoify",
"foo",
"fred",
"garply",
"grault",
"HKT",
"HKTs",
"kinded",
"typeguard",
"typeguards",
"sonarjs",
"denoify",
"litecoin",
"monero",
"codesandbox",
"builtins",
"foo",
"bar",
"baz",
"qux",
"quux",
"corge",
"grault",
"garply",
"waldo",
"fred",
"plugh",
"xyzzy",
"quux",
"qux",
"sonarjs",
"thud",
"typeguard",
"typeguards",
"waldo",
"xyzzy"
],
"overrides": [
{
Expand Down
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"/**/*.md"
],
"rules": {
"import/no-relative-parent-imports": "error",
"import/no-relative-parent-imports": "off",
"node/no-extraneous-import": ["error", {
"allowModules": ["deepmerge-ts"]
}],
Expand Down
2 changes: 1 addition & 1 deletion .markdownlint.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
// MD024/no-duplicate-heading/no-duplicate-header - Multiple headings with the same content
"MD024": { "siblings_only": true },
// MD025/single-title/single-h1 - Multiple top level headings in the same document
"MD025": true,
"MD025": false,
// MD026/no-trailing-punctuation - Trailing punctuation in heading
"MD026": true,
// MD027/no-multiple-space-blockquote - Multiple spaces after blockquote symbol
Expand Down
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,20 @@ console.log(merged);

You can try out this example at [codesandbox.io](https://codesandbox.io/s/deepmerge-ts-example-iltxby?file=/src/example.ts).

### Using customized config
### Merging into a Target

[See deepmerge custom docs](./docs/deepmergeCustom.md).
You can use `deepmergeInto` if you want to update a target object with the merge result instead of creating a new object.

This function is best used with objects that are all of the same type.

Note: If the target object's type is different to the input objects, we'll assert that the target's type has changed (this is not done automatically with `deepmergeIntoCustom`).

### Customized the Merging Process

We provide a customizer function for each of our main deepmerge functions: `deepmergeCustom` and `deepmergeIntoCustom`.
You can use these to customize the details of how values should be merged together.

See [deepmerge custom docs](./docs/deepmergeCustom.md) for more details.

## Performance

Expand All @@ -140,4 +151,4 @@ In addition to performance improvements, this strategy merges multiple inputs at

## API

[See API docs](./docs/API.md).
See [API docs](./docs/API.md).
106 changes: 99 additions & 7 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ 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`.

## deepmergeInto(target, value, ...)

Mutate the target by merging the other inputs into it using the default configuration.

## 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.
Generate a customized `deepmerge` function using the given options. The returned function works just like `deepmerge` except it uses the customized configuration.

### options

Expand All @@ -23,27 +27,27 @@ All these options are optional.

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.
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, meta: MetaData) => unknown`

If false, arrays won't be merged. If set to a function, that function will be used to merge arrays.
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, meta: MetaData) => unknown`

If false, maps won't be merged. If set to a function, that function will be used to merge maps.
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, meta: MetaData) => unknown`

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

#### `mergeOthers`

Expand All @@ -70,10 +74,98 @@ These will be the custom merge functions you gave, or the default merge function

#### `defaultMergeFunctions`

These are all the merge functions that the default, non-customize deepmerge function uses.
These are all the merge functions that the default, non-customized `deepmerge` function uses.

#### `metaDataUpdater`

This function is used to update the meta data. Call it with the new meta data when/where applicable.

#### `deepmerge`

This is your top level customized deepmerge function.
This is your top level customized `deepmerge` function.

Note: Be careful when calling this as it is really easy to end up in an infinite loop.

#### `useImplicitDefaultMerging`

States whether or not implicit default merging is in use.

#### `actions`

Contains symbols that can be used to tell `deepmerge-ts` to perform a special action.

## deepmergeIntoCustom(options[, rootMetaData])

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

### options

The following options can be used to customize the deepmerge function.\
All these options are optional.

#### `mergeRecords`

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

If `false`, records won't be merged. If set to a function, that function will be used to merge records by mutating `target.value`.

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

#### `mergeArrays`

Type: `false | (target: DeepMergeValueReference<unknown[]>, values: unknown[][], utils: DeepMergeMergeIntoFunctionUtils, meta: MetaData) => void | symbol`

If `false`, arrays won't be merged. If set to a function, that function will be used to merge arrays by mutating `target.value`.

#### `mergeMaps`

Type: `false | (target: DeepMergeValueReference<Map<unknown, unknown>>, values: Map<unknown, unknown>[], utils: DeepMergeMergeIntoFunctionUtils, meta: MetaData) => void | symbol`

If `false`, maps won't be merged. If set to a function, that function will be used to merge maps by mutating `target.value`.

#### `mergeSets`

Type: `false | (target: DeepMergeValueReference<Set<unknown>>, values: Set<unknown>[], utils: DeepMergeMergeIntoFunctionUtils, meta: MetaData) => void | symbol`

If `false`, sets won't be merged. If set to a function, that function will be used to merge sets by mutating `target.value`.

#### `mergeOthers`

Type: `(target: DeepMergeValueReference<unknown>, values: unknown[], utils: DeepMergeMergeIntoFunctionUtils, meta: MetaData) => void | symbol`

If set to a function, that function will be used to merge everything else by mutating `target.value`.

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.

### DeepMergeMergeIntoFunctionUtils

This is a set of utility functions that are made available to your custom merge functions.

#### `mergeFunctions`

These are all the merge function being used to perform the deepmerge.\
These will be the custom merge functions you gave, or the default merge functions for options you didn't customize.

#### `defaultMergeFunctions`

These are all the merge functions that the default, non-customized `deepmerge` function uses.

#### `metaDataUpdater`

This function is used to update the meta data. Call it with the new meta data when/where applicable.

#### `deepmergeInto`

This is your top level customized `deepmergeInto` function.

Note: Be careful when calling this as it is really easy to end up in an infinite loop.

#### `actions`

Contains symbols that can be used to tell `deepmerge-ts` to perform a special action.
75 changes: 73 additions & 2 deletions docs/deepmergeCustom.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const customizedDeepmerge = deepmergeCustom({
});
```

## Customizing the return type
## Customizing the Return Type

If you want to customize the deepmerge function, you probably also want the return type of the result to be correct too.\
Unfortunately however, due to TypeScript limitations, we can not automatically infer this.
Expand Down Expand Up @@ -298,4 +298,75 @@ declare module "../src/types" {

## API

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

# Deepmerge Into Custom

`deepmergeIntoCustom` as the name suggests, works just like `deepmergeCustom`, only for `deepmergeInto` instead of `deepmerge`.
But there are some differences to be aware of.

## Merge Functions

The signature of merging functions for `deepmergeIntoCustom` looks like this:

```ts
(target: DeepMergeValueReference<T>, values: Ts, utils: U, meta: M | undefined) => void | symbol;
```

Instead of returning a value like with `deepmergeCustom`'s merge functions, mutations should be made to `target.value`.\
You can however still return an action.

Note: `values` includes all the values, including the target's value (if there is one).

### Special Actions

#### No Skip Action (`utils.actions.skip`)

This action doesn't make sense with in the context of merging into a target.
Use `delete target.value[key]` instead if you don't want the property to exists on the target.

#### No Implicit Default Merging

It doesn't make sense to have implicit default merging here as all merge functions should return `undefined` (if not returning an action).

## Customizing the Return Type

The return type of a custom `deepmergeInto` should be void, so you don't need to customize it's return type like you would with a regular custom `deepmerge` function.

However, you may want to use an assertion function if the target's type is not the same as the inputs.
This is by no means required though.
But if you want to do this then you'll simply need to explicity declare a type annotation for your customized `deepmergeInto` function that makes such an assertion.

Here's an example:

```ts
type CustomizedDeepmergeInto = <
Target extends object,
Ts extends ReadonlyArray<object>
>(
target: Target,
...objects: Ts
) => asserts target is Target & // Unioning with `Target` is essentially required to make TypeScript happy.
DeepMergeHKT<
[Target, ...Ts], // Don't forget to pass the `Target` type here too.
{
DeepMergeRecordsURI: DeepMergeMergeFunctionsDefaultURIs["DeepMergeRecordsURI"]; // Use default behavior.
DeepMergeArraysURI: DeepMergeMergeFunctionsDefaultURIs["DeepMergeArraysURI"]; // Use default behavior.
DeepMergeSetsURI: DeepMergeMergeFunctionsDefaultURIs["DeepMergeSetsURI"]; // Use default behavior.
DeepMergeMapsURI: DeepMergeMergeFunctionsDefaultURIs["DeepMergeMapsURI"]; // Use default behavior.
DeepMergeOthersURI: "CustomDeepMergeOthersURI"; // Use custom behavior (see deepmergeCustom's docs above for details).
},
DeepMergeBuiltInMetaData // Use default meta data.
>;

export const customizedDeepmergeInto: CustomizedDeepmergeInto =
deepmergeIntoCustom({
mergeOthers: (source, values, utils, meta) => {
/* ... */
},
});
```

## API

See [deepmerge into custom API](./API.md#deepmergeintocustomoptions-rootmetadata).
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"@commitlint/cli": "17.4.2",
"@commitlint/config-conventional": "17.4.2",
"@cspell/dict-cryptocurrencies": "3.0.1",
"@rebeccastevens/eslint-config": "1.5.1",
"@rebeccastevens/eslint-config": "1.5.2",
"@rollup/plugin-json": "6.0.0",
"@rollup/plugin-node-resolve": "15.0.1",
"@rollup/plugin-typescript": "11.0.0",
Expand All @@ -117,7 +117,7 @@
"eslint-import-resolver-typescript": "3.5.3",
"eslint-plugin-ava": "14.0.0",
"eslint-plugin-eslint-comments": "3.2.0",
"eslint-plugin-functional": "5.0.3",
"eslint-plugin-functional": "5.0.4",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-jsdoc": "39.7.5",
"eslint-plugin-markdown": "3.0.0",
Expand Down
14 changes: 14 additions & 0 deletions src/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Special values that tell deepmerge to perform a certain action.
*/
export const actions = {
defaultMerge: Symbol("deepmerge-ts: default merge"),
skip: Symbol("deepmerge-ts: skip"),
} as const;

/**
* Special values that tell deepmergeInto to perform a certain action.
*/
export const actionsInto = {
defaultMerge: actions.defaultMerge,
} as const;

0 comments on commit 9c350a0

Please sign in to comment.