feat(mergeWith): Add implementation for mergeWith

This commit is contained in:
Sojin Park 2024-08-10 16:24:51 +09:00
parent bb94b88e70
commit cc3a467443
19 changed files with 872 additions and 84 deletions

View File

@ -150,6 +150,7 @@ function sidebar(): DefaultTheme.Sidebar {
{ text: 'mapKeys', link: '/reference/object/mapKeys' },
{ text: 'mapValues', link: '/reference/object/mapValues' },
{ text: 'merge', link: '/reference/object/merge' },
{ text: 'mergeWith', link: '/reference/object/mergeWith' },
{ text: 'toMerged', link: '/reference/object/toMerged' },
{ text: 'omit', link: '/reference/object/omit' },
{ text: 'omitBy', link: '/reference/object/omitBy' },

View File

@ -161,6 +161,7 @@ function sidebar(): DefaultTheme.Sidebar {
{ text: 'mapKeys', link: '/ko/reference/object/mapKeys' },
{ text: 'mapValues', link: '/ko/reference/object/mapValues' },
{ text: 'merge', link: '/ko/reference/object/merge' },
{ text: 'mergeWith', link: '/ko/reference/object/mergeWith' },
{ text: 'toMerged', link: '/ko/reference/object/toMerged' },
{ text: 'omit', link: '/ko/reference/object/omit' },
{ text: 'omitBy', link: '/ko/reference/object/omitBy' },

View File

@ -146,6 +146,7 @@ function sidebar(): DefaultTheme.Sidebar {
{ text: 'mapKeys', link: '/zh_hans/reference/object/mapKeys' },
{ text: 'mapValues', link: '/zh_hans/reference/object/mapValues' },
{ text: 'merge', link: '/zh_hans/reference/object/merge' },
{ text: 'mergeWith', link: '/zh_hans/reference/object/mergeWith' },
{ text: 'toMerged', link: '/zh_hans/reference/object/toMerged' },
{ text: 'omit', link: '/zh_hans/reference/object/omit' },
{ text: 'omitBy', link: '/zh_hans/reference/object/omitBy' },

View File

@ -300,8 +300,8 @@ Even if a feature is marked "in review," it might already be under review to ens
| [keysIn](https://lodash.com/docs/4.17.15#keysIn) | ❌ |
| [mapKeys](https://lodash.com/docs/4.17.15#mapKeys) | ✅ |
| [mapValues](https://lodash.com/docs/4.17.15#mapValues) | ✅ |
| [merge](https://lodash.com/docs/4.17.15#merge) | |
| [mergeWith](https://lodash.com/docs/4.17.15#mergeWith) | |
| [merge](https://lodash.com/docs/4.17.15#merge) | |
| [mergeWith](https://lodash.com/docs/4.17.15#mergeWith) | |
| [omit](https://lodash.com/docs/4.17.15#omit) | 📝 |
| [omitBy](https://lodash.com/docs/4.17.15#omitBy) | 📝 |
| [pick](https://lodash.com/docs/4.17.15#pick) | 📝 |

View File

@ -301,8 +301,8 @@ chunk([1, 2, 3, 4], 0);
| [keysIn](https://lodash.com/docs/4.17.15#keysIn) | ❌ |
| [mapKeys](https://lodash.com/docs/4.17.15#mapKeys) | ✅ |
| [mapValues](https://lodash.com/docs/4.17.15#mapValues) | ✅ |
| [merge](https://lodash.com/docs/4.17.15#merge) | |
| [mergeWith](https://lodash.com/docs/4.17.15#mergeWith) | |
| [merge](https://lodash.com/docs/4.17.15#merge) | |
| [mergeWith](https://lodash.com/docs/4.17.15#mergeWith) | |
| [omit](https://lodash.com/docs/4.17.15#omit) | 📝 |
| [omitBy](https://lodash.com/docs/4.17.15#omitBy) | 📝 |
| [pick](https://lodash.com/docs/4.17.15#pick) | 📝 |

View File

@ -0,0 +1,83 @@
# mergeWith
`source`가 가지고 있는 값들을 `target` 객체로 병합해요.
어떻게 프로퍼티를 병합하는지 지정하기 위해서 `merge` 함수 인자를 정의하세요. `merge` 함수는 병합되는 모든 프로퍼티에 대해서, 다음과 같은 인자를 가지고 호출돼요.
- `targetValue`: `target` 객체가 가지고 있는 값.
- `sourceValue`: `source` 객체가 가지고 있는 값.
- `key`: 병합되고 있는 프로퍼티 이름.
- `target`: `target` 객체.
- `source`: `source` 객체.
`merge` 함수 인자는 `target` 객체에 설정될 값을 반환해야 해요. 만약 `undefined`를 반환한다면, 기본적으로 두 값을 깊이 병합해요. 깊은 병합에서는, 중첩된 객체나 배열을 다음과 같이 재귀적으로 병합해요.
- `source``target`의 프로퍼티가 모두 객체 또는 배열이라면, 두 객체와 배열은 병합돼요.
- 만약에 `source`의 프로퍼티가 `undefined` 라면, `target`의 프로퍼티를 덮어씌우지 않아요.
이 함수는 `target` 객체를 수정해요.
## 인터페이스
```typescript
function merge<T, S>(target: T, source: S): T & S;
```
### 파라미터
- `target` (`T`): `source` 객체가 가지고 있는 프로퍼티를 병합할 객체. 이 객체는 함수에 의해 수정돼요.
- `source` (`S`): `target` 객체로 프로퍼티를 병합할 객체.
- `merge` (`(targetValue: any, sourceValue: any, key: string, target: T, source: S) => any`): 두 값을 어떻게 병합할지 정의하는 함수. 아래와 같은 인자로 호출돼요.
- `targetValue`: `target` 객체가 가지고 있는 값.
- `sourceValue`: `source` 객체가 가지고 있는 값.
- `key`: 병합되고 있는 프로퍼티 이름.
- `target`: `target` 객체.
- `source`: `source` 객체.
### 반환 값
(`T & S`): `source` 객체가 가지고 있는 프로퍼티가 병합된 `target` 객체.
## 예시
```typescript
const target = { a: 1, b: 2 };
const source = { b: 3, c: 4 };
mergeWith(target, source, (targetValue, sourceValue) => {
if (typeof targetValue === 'number' && typeof sourceValue === 'number') {
return targetValue + sourceValue;
}
});
// 반환 값: { a: 1, b: 5, c: 4 }
const target = { a: [1], b: [2] };
const source = { a: [3], b: [4] };
const result = mergeWith(target, source, (objValue, srcValue) => {
if (Array.isArray(objValue)) {
return objValue.concat(srcValue);
}
});
// 반환 값: { a: [1, 3], b: [2, 4] })
```
## 데모
::: sandpack
```ts index.ts
import { mergeWith } from 'es-toolkit';
const target = { a: 1, b: 2 };
const source = { b: 3, c: 4 };
const result = mergeWith(target, source, (targetValue, sourceValue) => {
if (typeof targetValue === 'number' && typeof sourceValue === 'number') {
return targetValue + sourceValue;
}
});
console.log(result);
```
:::

View File

@ -0,0 +1,90 @@
# mergeWith
Merges the properties of the source object into the target object.
You can provide a custom `merge` function to control how properties are merged. The `merge` function is called for each property that is being merged and receives the following arguments:
- `targetValue`: The current value of the property in the target object.
- `sourceValue`: The value of the property in the source object.
- `key`: The key of the property being merged.
- `target`: The target object.
- `source`: The source object.
The `merge` function should return the value to be set in the target object. If it returns `undefined`, a default deep merge will be applied for arrays and objects:
- If a property in the source object is an array or an object and the corresponding property in the target object is also an array or object, they will be merged.
- If a property in the source object is undefined, it will not overwrite a defined property in the target object.
See [merge](./merge.md) for details of a default deep merge.
This function mutates the target object.
## Signature
```typescript
function mergeWith<T, S>(
target: T,
source: S,
merge: (targetValue: any, sourceValue: any, key: string, target: T, source: S) => any
): T & S;
```
### Parameters
- `target` (`T`): The target object into which the source object properties will be merged. This object is modified in place.
- `source` (`S`): The source object whose properties will be merged into the target object.
- `merge` (`(targetValue: any, sourceValue: any, key: string, target: T, source: S) => any`): A custom merge function that defines how properties should be combined. It receives the following arguments:
- `targetValue`: The current value of the property in the target object.
- `sourceValue`: The value of the property in the source object.
- `key`: The key of the property being merged.
- `target`: The target object.
- `source`: The source object.
### Returns
(`T & S`): The updated target object with properties from the source object merged in.
## Examples
```typescript
const target = { a: 1, b: 2 };
const source = { b: 3, c: 4 };
mergeWith(target, source, (targetValue, sourceValue) => {
if (typeof targetValue === 'number' && typeof sourceValue === 'number') {
return targetValue + sourceValue;
}
});
// Returns { a: 1, b: 5, c: 4 }
const target = { a: [1], b: [2] };
const source = { a: [3], b: [4] };
const result = mergeWith(target, source, (objValue, srcValue) => {
if (Array.isArray(objValue)) {
return objValue.concat(srcValue);
}
});
// Returns { a: [1, 3], b: [2, 4] })
```
## Demo
::: sandpack
```ts index.ts
import { mergeWith } from 'es-toolkit';
const target = { a: 1, b: 2 };
const source = { b: 3, c: 4 };
const result = mergeWith(target, source, (targetValue, sourceValue) => {
if (typeof targetValue === 'number' && typeof sourceValue === 'number') {
return targetValue + sourceValue;
}
});
console.log(result);
```
:::

View File

@ -301,8 +301,8 @@ chunk([1, 2, 3, 4], 0);
| [mapKeys](https://lodash.com/docs/4.17.15#mapKeys) | ✅ |
| [mapValues](https://lodash.com/docs/4.17.15#mapValues) | ✅ |
| [mapValues](https://lodash.com/docs/4.17.15#mapValues) | ❌ |
| [merge](https://lodash.com/docs/4.17.15#merge) | |
| [mergeWith](https://lodash.com/docs/4.17.15#mergeWith) | |
| [merge](https://lodash.com/docs/4.17.15#merge) | |
| [mergeWith](https://lodash.com/docs/4.17.15#mergeWith) | |
| [omit](https://lodash.com/docs/4.17.15#omit) | 📝 |
| [omitBy](https://lodash.com/docs/4.17.15#omitBy) | 📝 |
| [pick](https://lodash.com/docs/4.17.15#pick) | 📝 |

View File

@ -0,0 +1,91 @@
# mergeWith
将源对象的属性合并到目标对象中。
您可以提供自定义的 `merge` 函数来控制属性的合并方式。`merge` 函数会在每个属性被合并时调用,并接收以下参数:
- `targetValue`:目标对象中属性的当前值。
- `sourceValue`:源对象中属性的值。
- `key`:被合并的属性的键。
- `target`:目标对象。
- `source`:源对象。
`merge` 函数应返回要在目标对象中设置的值。如果返回 `undefined`,则会对数组和对象应用默认的深度合并:
- 如果源对象中的属性是数组或对象,而目标对象中对应的属性也是数组或对象,它们将被合并。
- 如果源对象中的属性是 `undefined`,它不会覆盖目标对象中已定义的属性。
有关默认深度合并的详细信息,请参见 [merge](./merge.md)。
此函数会修改目标对象。
## 签名
```typescript
function mergeWith<T, S>(
target: T,
source: S,
merge: (targetValue: any, sourceValue: any, key: string, target: T, source: S) => any
): T & S;
```
### 参数
- `target` (`T`): 目标对象,源对象的属性将合并到此对象中。该对象会被就地修改。
- `source` (`S`): 源对象,其属性将合并到目标对象中。
- `merge` (`(targetValue: any, sourceValue: any, key: string, target: T, source: S) => any`): 自定义合并函数,用于定义属性如何组合。它接收以下参数:
- `targetValue`: 目标对象中属性的当前值。
- `sourceValue`: 源对象中属性的值。
- `key`: 被合并的属性的键。
- `target`: 目标对象。
- `source`: 源对象。
### 返回值
(`T & S`): 合并了源对象属性的更新后的目标对象。
## 示例
```typescript
const target = { a: 1, b: 2 };
const source = { b: 3, c: 4 };
mergeWith(target, source, (targetValue, sourceValue) => {
if (typeof targetValue === 'number' && typeof sourceValue === 'number') {
return targetValue + sourceValue;
}
});
// 返回 { a: 1, b: 5, c: 4 }
const target = { a: [1], b: [2] };
const source = { a: [3], b: [4] };
const result = mergeWith(target, source, (objValue, srcValue) => {
if (Array.isArray(objValue)) {
return objValue.concat(srcValue);
}
});
// 返回 { a: [1, 3], b: [2, 4] })
```
## 演示
::: sandpack
```typescript
import { mergeWith } from 'es-toolkit';
const target = { a: 1, b: 2 };
const source = { b: 3, c: 4 };
const result = mergeWith(target, source, (targetValue, sourceValue) => {
if (typeof targetValue === 'number' && typeof sourceValue === 'number') {
return targetValue + sourceValue;
}
});
console.log(result);
```
:::

View File

@ -44,6 +44,7 @@ export { property } from './object/property.ts';
export { mapKeys } from './object/mapKeys.ts';
export { mapValues } from './object/mapValues.ts';
export { merge } from './object/merge.ts';
export { mergeWith } from './object/mergeWith.ts';
export { isPlainObject } from './predicate/isPlainObject.ts';
export { isArray } from './predicate/isArray.ts';

View File

@ -47,8 +47,6 @@ describe('merge', () => {
const actual = merge(object, source);
console.log(actual);
expect(actual.bar.b).not.toBe(actual.foo.b);
expect(actual.foo.b.c.d).toBe(actual.foo.b.c.d.foo.b.c.d);
});
@ -144,7 +142,7 @@ describe('merge', () => {
expected = { 0: 1, 1: 2, 2: 3 };
actual = merge({}, object1);
console.log(expected, actual.value);
expect(isArguments(actual.value)).toBe(false);
expect(actual.value).toEqual(expected);
});

View File

@ -1,9 +1,5 @@
import { clone } from '../../object/clone.ts';
import { isArguments } from '../predicate/isArguments.ts';
import { isObjectLike } from '../predicate/isObjectLike.ts';
import { isPlainObject } from '../predicate/isPlainObject.ts';
import { isTypedArray } from '../predicate/isTypedArray.ts';
import { cloneDeep } from './cloneDeep.ts';
import { noop } from '../../function/noop.ts';
import { mergeWith } from './mergeWith.ts';
declare var Buffer:
| {
@ -237,73 +233,5 @@ export function merge<O, S1, S2, S3, S4>(
export function merge(object: any, ...sources: any[]): any;
export function merge(object: any, ...sources: any[]): any {
let result = object;
for (let i = 0; i < sources.length; i++) {
const source = sources[i];
result = mergeDeep(object, source, new Map());
}
return result;
}
function mergeDeep(object: any, source: any, stack: Map<any, any>) {
if (source == null || typeof source !== 'object') {
return object;
}
if (stack.has(source)) {
return clone(stack.get(source));
}
stack.set(source, object);
if (Array.isArray(source)) {
source = source.slice();
for (let i = 0; i < source.length; i++) {
source[i] = source[i] ?? undefined;
}
}
const sourceKeys = Object.keys(source);
for (let i = 0; i < sourceKeys.length; i++) {
const key = sourceKeys[i];
let sourceValue = source[key];
let objectValue = object[key];
if (isArguments(sourceValue)) {
sourceValue = { ...sourceValue };
}
if (isArguments(objectValue)) {
objectValue = { ...objectValue };
}
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(sourceValue)) {
sourceValue = cloneDeep(sourceValue);
}
if (Array.isArray(sourceValue)) {
objectValue = typeof objectValue === 'object' ? Array.from(objectValue ?? []) : [];
}
if (Array.isArray(sourceValue)) {
object[key] = mergeDeep(objectValue, sourceValue, stack);
} else if (isObjectLike(objectValue) && isObjectLike(sourceValue)) {
object[key] = mergeDeep(objectValue, sourceValue, stack);
} else if (objectValue == null && Array.isArray(sourceValue)) {
object[key] = mergeDeep([], sourceValue, stack);
} else if (objectValue == null && isPlainObject(sourceValue)) {
object[key] = mergeDeep({}, sourceValue, stack);
} else if (objectValue == null && isTypedArray(sourceValue)) {
object[key] = cloneDeep(sourceValue);
} else if (objectValue === undefined || sourceValue !== undefined) {
object[key] = sourceValue;
}
}
return object;
return mergeWith(object, ...sources, noop);
}

View File

@ -0,0 +1,59 @@
import { describe, expect, it } from 'vitest';
import { noop } from '../../function/noop';
import { identity } from '../_internal/identity';
import { mergeWith } from './mergeWith';
import { last } from '../../array/last';
describe('mergeWith', () => {
it('should handle merging when `customizer` returns `undefined`', () => {
let actual: any = mergeWith({ a: { b: [1, 1] } }, { a: { b: [0] } }, noop);
expect(actual).toEqual({ a: { b: [0, 1] } });
actual = mergeWith([], [undefined], identity);
expect(actual).toEqual([undefined]);
});
it('should clone sources when `customizer` returns `undefined`', () => {
const source1 = { a: { b: { c: 1 } } };
const source2 = { a: { b: { d: 2 } } };
mergeWith({}, source1, source2, noop);
expect(source1.a.b).toEqual({ c: 1 });
});
it('should defer to `customizer` for non `undefined` results', () => {
const actual = mergeWith({ a: { b: [0, 1] } }, { a: { b: [2] } }, (a, b) =>
Array.isArray(a) ? a.concat(b) : undefined
);
expect(actual).toEqual({ a: { b: [0, 1, 2] } });
});
it('should provide `stack` to `customizer`', () => {
let actual: any;
mergeWith({}, { a: { b: 2 } }, function () {
// eslint-disable-next-line
// @ts-ignore
actual = last(arguments);
});
expect(actual instanceof Map).toBe(true);
});
it('should overwrite primitives with source object clones', () => {
const actual = mergeWith({ a: 0 }, { a: { b: ['c'] } }, (a, b) => (Array.isArray(a) ? a.concat(b) : undefined));
expect(actual).toEqual({ a: { b: ['c'] } });
});
it('should pop the stack of sources for each sibling property', () => {
const array = ['b', 'c'];
const object = { a: ['a'] };
const source = { a: array, b: array };
const actual = mergeWith(object, source, (a, b) => (Array.isArray(a) ? a.concat(b) : undefined));
expect(actual).toEqual({ a: ['a', 'b', 'c'], b: ['b', 'c'] });
});
});

View File

@ -0,0 +1,389 @@
import { clone } from '../../object/clone.ts';
import { isArguments } from '../predicate/isArguments.ts';
import { isObjectLike } from '../predicate/isObjectLike.ts';
import { isPlainObject } from '../predicate/isPlainObject.ts';
import { isTypedArray } from '../predicate/isTypedArray.ts';
import { cloneDeep } from './cloneDeep.ts';
declare var Buffer:
| {
isBuffer: (a: any) => boolean;
}
| undefined;
/**
* Merges the properties of one or more source objects into the target object.
*
* This function performs a deep merge, recursively merging nested objects and arrays.
* If a property in the source object is an array or object and the corresponding property in the target object is also an array or object, they will be merged.
* If a property in the source object is `undefined`, it will not overwrite a defined property in the target object.
*
* You can provide a custom `merge` function to control how properties are merged. The `merge` function is called for each property that is being merged and receives the following arguments:
*
* - `targetValue`: The current value of the property in the target object.
* - `sourceValue`: The value of the property in the source object.
* - `key`: The key of the property being merged.
* - `target`: The target object.
* - `source`: The source object.
* - `stack`: A `Map` used to keep track of objects that have already been processed to handle circular references.
*
* The `merge` function should return the value to be set in the target object. If it returns `undefined`, a default deep merge will be applied for arrays and objects.
*
* The function can handle multiple source objects and will merge them all into the target object.
*
* @param {T} target - The target object into which the source object properties will be merged. This object is modified in place.
* @param {S} source - The first source object whose properties will be merged into the target object.
* @returns {T & S} The updated target object with properties from the source object(s) merged in.
*
* @template T - Type of the target object.
* @template S - Type of the first source object.
*
* @example
* const target = { a: 1, b: 2 };
* const source = { b: 3, c: 4 };
*
* mergeWith(target, source, (targetValue, sourceValue) => {
* if (typeof targetValue === 'number' && typeof sourceValue === 'number') {
* return targetValue + sourceValue;
* }
* });
* // Returns { a: 1, b: 5, c: 4 }
* @example
* const target = { a: [1], b: [2] };
* const source = { a: [3], b: [4] };
*
* const result = mergeWith(target, source, (objValue, srcValue) => {
* if (Array.isArray(objValue)) {
* return objValue.concat(srcValue);
* }
* });
*
* expect(result).toEqual({ a: [1, 3], b: [2, 4] });
*/
export function mergeWith<T, S>(
target: T,
source: S,
merge: (targetValue: any, sourceValue: any, key: string, target: T, source: S, stack: Map<any, any>) => any
): T & S;
/**
* Merges the properties of one or more source objects into the target object.
*
* This function performs a deep merge, recursively merging nested objects and arrays.
* If a property in the source object is an array or object and the corresponding property in the target object is also an array or object, they will be merged.
* If a property in the source object is `undefined`, it will not overwrite a defined property in the target object.
*
* You can provide a custom `merge` function to control how properties are merged. The `merge` function is called for each property that is being merged and receives the following arguments:
*
* - `targetValue`: The current value of the property in the target object.
* - `sourceValue`: The value of the property in the source object.
* - `key`: The key of the property being merged.
* - `target`: The target object.
* - `source`: The source object.
* - `stack`: A `Map` used to keep track of objects that have already been processed to handle circular references.
*
* The `merge` function should return the value to be set in the target object. If it returns `undefined`, a default deep merge will be applied for arrays and objects.
*
* The function can handle multiple source objects and will merge them all into the target object.
*
* @param {O} object - The target object into which the source object properties will be merged. This object is modified in place.
* @param {S1} source1 - The first source object to be merged into the target object.
* @param {S2} source2 - The second source object to be merged into the target object.
* @returns {O & S1 & S2} The updated target object with properties from the source objects merged in.
*
* @template O - Type of the target object.
* @template S1 - Type of the first source object.
* @template S2 - Type of the second source object.
*
* @example
* const target = { a: 1, b: 2 };
* const source = { b: 3, c: 4 };
*
* mergeWith(target, source, (targetValue, sourceValue) => {
* if (typeof targetValue === 'number' && typeof sourceValue === 'number') {
* return targetValue + sourceValue;
* }
* });
* // Returns { a: 1, b: 5, c: 4 }
* @example
* const target = { a: [1], b: [2] };
* const source = { a: [3], b: [4] };
*
* const result = mergeWith(target, source, (objValue, srcValue) => {
* if (Array.isArray(objValue)) {
* return objValue.concat(srcValue);
* }
* });
*
* expect(result).toEqual({ a: [1, 3], b: [2, 4] });
*/
export function mergeWith<O, S1, S2>(
object: O,
source1: S1,
source2: S2,
merge: (targetValue: any, sourceValue: any, key: string, target: any, source: any, stack: Map<any, any>) => any
): O & S1 & S2;
/**
* Merges the properties of one or more source objects into the target object.
*
* This function performs a deep merge, recursively merging nested objects and arrays.
* If a property in the source object is an array or object and the corresponding property in the target object is also an array or object, they will be merged.
* If a property in the source object is `undefined`, it will not overwrite a defined property in the target object.
*
* You can provide a custom `merge` function to control how properties are merged. The `merge` function is called for each property that is being merged and receives the following arguments:
*
* - `targetValue`: The current value of the property in the target object.
* - `sourceValue`: The value of the property in the source object.
* - `key`: The key of the property being merged.
* - `target`: The target object.
* - `source`: The source object.
* - `stack`: A `Map` used to keep track of objects that have already been processed to handle circular references.
*
* The `merge` function should return the value to be set in the target object. If it returns `undefined`, a default deep merge will be applied for arrays and objects.
*
* The function can handle multiple source objects and will merge them all into the target object.
*
* @param {O} object - The target object into which the source object properties will be merged. This object is modified in place.
* @param {S1} source1 - The first source object whose properties will be merged into the target object.
* @param {S2} source2 - The second source object whose properties will be merged into the target object.
* @param {S3} source3 - The third source object whose properties will be merged into the target object.
* @returns {O & S1 & S2 & S3} The updated target object with properties from the source object(s) merged in.
*
* @template O - Type of the target object.
* @template S1 - Type of the first source object.
* @template S2 - Type of the second source object.
* @template S3 - Type of the third source object.
*
* @example
* const target = { a: 1, b: 2 };
* const source = { b: 3, c: 4 };
*
* mergeWith(target, source, (targetValue, sourceValue) => {
* if (typeof targetValue === 'number' && typeof sourceValue === 'number') {
* return targetValue + sourceValue;
* }
* });
* // Returns { a: 1, b: 5, c: 4 }
* @example
* const target = { a: [1], b: [2] };
* const source = { a: [3], b: [4] };
*
* const result = mergeWith(target, source, (objValue, srcValue) => {
* if (Array.isArray(objValue)) {
* return objValue.concat(srcValue);
* }
* });
*
* expect(result).toEqual({ a: [1, 3], b: [2, 4] });
*/
export function mergeWith<O, S1, S2, S3>(
object: O,
source1: S1,
source2: S2,
source3: S3,
merge: (targetValue: any, sourceValue: any, key: string, target: any, source: any, stack: Map<any, any>) => any
): O & S1 & S2 & S3;
/**
* Merges the properties of one or more source objects into the target object.
*
* This function performs a deep merge, recursively merging nested objects and arrays.
* If a property in the source object is an array or object and the corresponding property in the target object is also an array or object, they will be merged.
* If a property in the source object is `undefined`, it will not overwrite a defined property in the target object.
*
* You can provide a custom `merge` function to control how properties are merged. The `merge` function is called for each property that is being merged and receives the following arguments:
*
* - `targetValue`: The current value of the property in the target object.
* - `sourceValue`: The value of the property in the source object.
* - `key`: The key of the property being merged.
* - `target`: The target object.
* - `source`: The source object.
* - `stack`: A `Map` used to keep track of objects that have already been processed to handle circular references.
*
* The `merge` function should return the value to be set in the target object. If it returns `undefined`, a default deep merge will be applied for arrays and objects.
*
* The function can handle multiple source objects and will merge them all into the target object.
*
* @param {O} object - The target object into which the source object properties will be merged. This object is modified in place.
* @param {S1} source1 - The first source object whose properties will be merged into the target object.
* @param {S2} source2 - The second source object whose properties will be merged into the target object.
* @param {S3} source3 - The third source object whose properties will be merged into the target object.
* @param {S4} source4 - The fourth source object whose properties will be merged into the target object.
* @returns {O & S1 & S2 & S3 & S4} The updated target object with properties from the source object(s) merged in.
*
* @template O - Type of the target object.
* @template S1 - Type of the first source object.
* @template S2 - Type of the second source object.
* @template S3 - Type of the third source object.
* @template S4 - Type of the fourth source object.
*
* @example
* const target = { a: 1, b: 2 };
* const source = { b: 3, c: 4 };
*
* mergeWith(target, source, (targetValue, sourceValue) => {
* if (typeof targetValue === 'number' && typeof sourceValue === 'number') {
* return targetValue + sourceValue;
* }
* });
* // Returns { a: 1, b: 5, c: 4 }
* @example
* const target = { a: [1], b: [2] };
* const source = { a: [3], b: [4] };
*
* const result = mergeWith(target, source, (objValue, srcValue) => {
* if (Array.isArray(objValue)) {
* return objValue.concat(srcValue);
* }
* });
*
* expect(result).toEqual({ a: [1, 3], b: [2, 4] });
*/
export function mergeWith<O, S1, S2, S3, S4>(
object: O,
source1: S1,
source2: S2,
source3: S3,
source4: S4,
merge: (targetValue: any, sourceValue: any, key: string, target: any, source: any, stack: Map<any, any>) => any
): O & S1 & S2 & S3;
/**
* Merges the properties of one or more source objects into the target object.
*
* This function performs a deep merge, recursively merging nested objects and arrays.
* If a property in the source object is an array or object and the corresponding property in the target object is also an array or object, they will be merged.
* If a property in the source object is `undefined`, it will not overwrite a defined property in the target object.
*
* You can provide a custom `merge` function to control how properties are merged. The `merge` function is called for each property that is being merged and receives the following arguments:
*
* - `targetValue`: The current value of the property in the target object.
* - `sourceValue`: The value of the property in the source object.
* - `key`: The key of the property being merged.
* - `target`: The target object.
* - `source`: The source object.
* - `stack`: A `Map` used to keep track of objects that have already been processed to handle circular references.
*
* The `merge` function should return the value to be set in the target object. If it returns `undefined`, a default deep merge will be applied for arrays and objects.
*
* The function can handle multiple source objects and will merge them all into the target object.
*
* @param {any} any - The target object into which the source object properties will be merged. This object is modified in place.
* @param {any[]} sources - The source objects whose properties will be merged into the target object.
* @returns {any} The updated target object with properties from the source object(s) merged in.
*
* @example
* const target = { a: 1, b: 2 };
* const source = { b: 3, c: 4 };
*
* mergeWith(target, source, (targetValue, sourceValue) => {
* if (typeof targetValue === 'number' && typeof sourceValue === 'number') {
* return targetValue + sourceValue;
* }
* });
* // Returns { a: 1, b: 5, c: 4 }
* @example
* const target = { a: [1], b: [2] };
* const source = { a: [3], b: [4] };
*
* const result = mergeWith(target, source, (objValue, srcValue) => {
* if (Array.isArray(objValue)) {
* return objValue.concat(srcValue);
* }
* });
*
* expect(result).toEqual({ a: [1, 3], b: [2, 4] });
*/
export function mergeWith(object: any, ...otherArgs: any[]): any;
export function mergeWith(object: any, ...otherArgs: any[]): any {
const sources = otherArgs.slice(0, -1);
const merge = otherArgs[otherArgs.length - 1] as (
targetValue: any,
sourceValue: any,
key: string,
target: any,
source: any,
stack: Map<any, any>
) => any;
let result = object;
for (let i = 0; i < sources.length; i++) {
const source = sources[i];
result = mergeWithDeep(object, source, merge, new Map());
}
return result;
}
function mergeWithDeep(
target: any,
source: any,
merge: (targetValue: any, sourceValue: any, key: string, target: any, source: any, stack: Map<any, any>) => any,
stack: Map<any, any>
) {
if (source == null || typeof source !== 'object') {
return target;
}
if (stack.has(source)) {
return clone(stack.get(source));
}
stack.set(source, target);
if (Array.isArray(source)) {
source = source.slice();
for (let i = 0; i < source.length; i++) {
source[i] = source[i] ?? undefined;
}
}
const sourceKeys = Object.keys(source);
for (let i = 0; i < sourceKeys.length; i++) {
const key = sourceKeys[i];
let sourceValue = source[key];
let targetValue = target[key];
if (isArguments(sourceValue)) {
sourceValue = { ...sourceValue };
}
if (isArguments(targetValue)) {
targetValue = { ...targetValue };
}
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(sourceValue)) {
sourceValue = cloneDeep(sourceValue);
}
if (Array.isArray(sourceValue)) {
targetValue = typeof targetValue === 'object' ? Array.from(targetValue ?? []) : [];
}
const merged = merge(targetValue, sourceValue, key, target, source, stack);
if (merged != null) {
target[key] = merged;
} else if (Array.isArray(sourceValue)) {
target[key] = mergeWithDeep(targetValue, sourceValue, merge, stack);
} else if (isObjectLike(targetValue) && isObjectLike(sourceValue)) {
target[key] = mergeWithDeep(targetValue, sourceValue, merge, stack);
} else if (targetValue == null && Array.isArray(sourceValue)) {
target[key] = mergeWithDeep([], sourceValue, merge, stack);
} else if (targetValue == null && isPlainObject(sourceValue)) {
target[key] = mergeWithDeep({}, sourceValue, merge, stack);
} else if (targetValue == null && isTypedArray(sourceValue)) {
target[key] = cloneDeep(sourceValue);
} else if (targetValue === undefined || sourceValue !== undefined) {
target[key] = sourceValue;
}
}
return target;
}

View File

@ -9,3 +9,4 @@ export { mapKeys } from './mapKeys.ts';
export { mapValues } from './mapValues.ts';
export { cloneDeep } from './cloneDeep.ts';
export { merge } from './merge.ts';
export { mergeWith } from './mergeWith.ts';

View File

@ -7,6 +7,8 @@ import { isObjectLike } from '../compat/predicate/isObjectLike.ts';
* If a property in the source object is an array or an object and the corresponding property in the target object is also an array or object, they will be merged.
* If a property in the source object is undefined, it will not overwrite a defined property in the target object.
*
* Note that this function mutates the target object.
*
* @param {T} target - The target object into which the source object properties will be merged. This object is modified in place.
* @param {S} source - The source object whose properties will be merged into the target object.
* @returns {T & S} The updated target object with properties from the source object merged in.

View File

@ -0,0 +1,52 @@
import { describe, expect, it } from 'vitest';
import { mergeWith } from './mergeWith';
describe('mergeWith', () => {
it('should merge properties from source object into target object using custom merge function', () => {
const target1 = { a: 1, b: 2 };
const source1 = { b: 3, c: 4 };
const result1 = mergeWith(target1, source1, (targetValue, sourceValue) => {
if (typeof targetValue === 'number' && typeof sourceValue === 'number') {
return targetValue + sourceValue;
}
});
expect(result1).toEqual({ a: 1, b: 5, c: 4 });
const target2 = { a: [1], b: [2] };
const source2 = { a: [3], b: [4] };
const result2 = mergeWith(target2, source2, (objValue, srcValue) => {
if (Array.isArray(objValue)) {
return objValue.concat(srcValue);
}
});
expect(result2).toEqual({ a: [1, 3], b: [2, 4] });
});
it('should use custom merge function for nested objects', () => {
const target = { a: { x: 1, y: 1 }, b: 2 };
const source = { a: { y: 2 }, b: 3 };
const result = mergeWith(target, source, (targetValue, sourceValue) => {
if (typeof targetValue === 'number' && typeof sourceValue === 'number') {
return targetValue + sourceValue;
}
});
expect(result).toEqual({ a: { x: 1, y: 3 }, b: 5 });
const target2 = { a: { c: [1] }, b: [2] };
const source2 = { a: { c: [3] }, b: [4] };
const result2 = mergeWith(target2, source2, (objValue, srcValue) => {
if (Array.isArray(objValue)) {
return objValue.concat(srcValue);
}
});
expect(result2).toEqual({ a: { c: [1, 3] }, b: [2, 4] });
});
});

89
src/object/mergeWith.ts Normal file
View File

@ -0,0 +1,89 @@
import { isObjectLike } from '../compat/predicate/isObjectLike.ts';
/**
* Merges the properties of the source object into the target object.
*
* You can provide a custom `merge` function to control how properties are merged. The `merge` function is called for each property that is being merged and receives the following arguments:
*
* - `targetValue`: The current value of the property in the target object.
* - `sourceValue`: The value of the property in the source object.
* - `key`: The key of the property being merged.
* - `target`: The target object.
* - `source`: The source object.
*
* The `merge` function should return the value to be set in the target object. If it returns `undefined`, a default deep merge will be applied for arrays and objects:
*
* - If a property in the source object is an array or an object and the corresponding property in the target object is also an array or object, they will be merged.
* - If a property in the source object is undefined, it will not overwrite a defined property in the target object.
*
* Note that this function mutates the target object.
*
* @param {T} target - The target object into which the source object properties will be merged. This object is modified in place.
* @param {S} source - The source object whose properties will be merged into the target object.
* @param {(targetValue: any, sourceValue: any, key: string, target: T, source: S) => any} merge - A custom merge function that defines how properties should be combined. It receives the following arguments:
* - `targetValue`: The current value of the property in the target object.
* - `sourceValue`: The value of the property in the source object.
* - `key`: The key of the property being merged.
* - `target`: The target object.
* - `source`: The source object.
*
* @returns {T & S} The updated target object with properties from the source object merged in.
*
* @template T - Type of the target object.
* @template S - Type of the source object.
*
* @example
* const target = { a: 1, b: 2 };
* const source = { b: 3, c: 4 };
*
* mergeWith(target, source, (targetValue, sourceValue) => {
* if (typeof targetValue === 'number' && typeof sourceValue === 'number') {
* return targetValue + sourceValue;
* }
* });
* // Returns { a: 1, b: 5, c: 4 }
* @example
* const target = { a: [1], b: [2] };
* const source = { a: [3], b: [4] };
*
* const result = mergeWith(target, source, (objValue, srcValue) => {
* if (Array.isArray(objValue)) {
* return objValue.concat(srcValue);
* }
* });
*
* expect(result).toEqual({ a: [1, 3], b: [2, 4] });
*/
export function mergeWith<T, S>(
target: T,
source: S,
merge: (targetValue: any, sourceValue: any, key: string, target: T, source: S) => any
): T & S;
export function mergeWith(
target: any,
source: any,
merge: (targetValue: any, sourceValue: any, key: string, target: any, source: any) => any
) {
const sourceKeys = Object.keys(source);
for (let i = 0; i < sourceKeys.length; i++) {
const key = sourceKeys[i];
const sourceValue = source[key];
const targetValue = target[key];
const merged = merge(targetValue, sourceValue, key, target, source);
if (merged != null) {
target[key] = merged;
} else if (Array.isArray(sourceValue)) {
target[key] = mergeWith(targetValue ?? [], sourceValue, merge);
} else if (isObjectLike(targetValue) && isObjectLike(sourceValue)) {
target[key] = mergeWith(targetValue ?? {}, sourceValue, merge);
} else if (targetValue === undefined || sourceValue !== undefined) {
target[key] = sourceValue;
}
}
return target;
}

View File

@ -10,6 +10,8 @@ import { merge } from './merge.ts';
* - If a property in the source object is an array or object and the corresponding property in the target object is also an array or object, they will be merged.
* - If a property in the source object is undefined, it will not overwrite a defined property in the target object.
*
* Note that this function does not mutate the target object.
*
* @param {T} target - The target object to be cloned and merged into. This object is not modified directly.
* @param {S} source - The source object whose properties will be merged into the cloned target object.
* @returns {T & S} A new object with properties from the source object merged into a deep clone of the target object.