feat(merge, toMerged): Add merge & toMerged

This commit is contained in:
Sojin Park 2024-08-10 15:38:00 +09:00
parent bff10652b4
commit 0054dc8119
20 changed files with 1361 additions and 1 deletions

View File

@ -0,0 +1,19 @@
import { describe, expect, it } from 'vitest';
import { getBundleSize } from './utils/getBundleSize';
describe('camelCase bundle size', () => {
it('lodash-es', async () => {
const bundleSize = await getBundleSize('lodash-es', 'camelCase');
expect(bundleSize).toMatchInlineSnapshot(`7293`);
});
it('es-toolkit', async () => {
const bundleSize = await getBundleSize('es-toolkit', 'merge');
expect(bundleSize).toMatchInlineSnapshot(`252`);
});
it('es-toolkit/compat', async () => {
const bundleSize = await getBundleSize('es-toolkit/compat', 'merge');
expect(bundleSize).toMatchInlineSnapshot(`4256`);
});
});

View File

@ -0,0 +1,26 @@
import { bench, describe } from 'vitest';
import { merge as mergeToolkit } from 'es-toolkit';
import { merge as mergeCompatToolkit } from 'es-toolkit/compat';
import { merge as mergeLodash } from 'lodash';
const object = {
a: [{ b: 2 }, { d: 4 }],
};
const other = {
a: [{ c: 3 }, { e: 5 }],
};
describe('merge', () => {
bench('lodash/merge', () => {
mergeLodash(object, other);
});
bench('es-toolkit/merge', () => {
mergeToolkit(object, other);
});
bench('es-toolkit/compat/merge', () => {
mergeCompatToolkit(object, other);
});
});

View File

@ -149,6 +149,8 @@ function sidebar(): DefaultTheme.Sidebar {
{ text: 'flattenObject', link: '/reference/object/flattenObject' },
{ text: 'mapKeys', link: '/reference/object/mapKeys' },
{ text: 'mapValues', link: '/reference/object/mapValues' },
{ text: 'merge', link: '/reference/object/merge' },
{ text: 'toMerged', link: '/reference/object/toMerged' },
{ text: 'omit', link: '/reference/object/omit' },
{ text: 'omitBy', link: '/reference/object/omitBy' },
{ text: 'pick', link: '/reference/object/pick' },

View File

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

View File

@ -145,6 +145,8 @@ function sidebar(): DefaultTheme.Sidebar {
{ text: 'flattenObject', link: '/zh_hans/reference/object/flattenObject' },
{ 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: 'toMerged', link: '/zh_hans/reference/object/toMerged' },
{ text: 'omit', link: '/zh_hans/reference/object/omit' },
{ text: 'omitBy', link: '/zh_hans/reference/object/omitBy' },
{ text: 'pick', link: '/zh_hans/reference/object/pick' },

View File

@ -0,0 +1,62 @@
# merge
`source`가 가지고 있는 값들을 `target` 객체로 병합해요.
이 함수는 깊은 병합을 수행하는데요, 중첩된 객체나 배열도 재귀적으로 병합돼요.
- `source``target`의 프로퍼티가 모두 객체 또는 배열이라면, 두 객체와 배열은 병합돼요.
- 만약에 `source`의 프로퍼티가 `undefined` 라면, `target`의 프로퍼티를 덮어씌우지 않아요.
[toMerged](./toMerged.md)와 다르게, 이 함수는 `target` 객체를 수정해요.
## 인터페이스
```typescript
function merge<T, S>(target: T, source: S): T & S;
```
### 파라미터
- `target` (`T`): `source` 객체가 가지고 있는 프로퍼티를 병합할 객체. 이 객체는 함수에 의해 수정돼요.
- `source` (`S`): `target` 객체로 프로퍼티를 병합할 객체.
### 반환 값
(`T & S`): `source` 객체가 가지고 있는 프로퍼티가 병합된 `target` 객체.
## 예시
```typescript
const target = { a: 1, b: { x: 1, y: 2 } };
const source = { b: { y: 3, z: 4 }, c: 5 };
const result = merge(target, source);
console.log(result);
// 반환 값: { a: 1, b: { x: 1, y: 3, z: 4 }, c: 5 }
const target = { a: [1, 2], b: { x: 1 } };
const source = { a: [3], b: { y: 2 } };
const result = merge(target, source);
console.log(result);
// 반환 값: { a: [3, 2], b: { x: 1, y: 2 } }
const target = { a: null };
const source = { a: [1, 2, 3] };
const result = merge(target, source);
console.log(result);
// 반환 값: { a: [1, 2, 3] }
```
## 데모
::: sandpack
```ts index.ts
import { merge } from 'es-toolkit';
const target = { a: 1, b: { x: 1, y: 2 } };
const source = { b: { y: 3, z: 4 }, c: 5 };
const result = merge(target, source);
console.log(result);
```
:::

View File

@ -0,0 +1,62 @@
# toMerged
`source`가 가지고 있는 값들을 `target` 객체로 병합해요.
이 함수는 깊은 병합을 수행하는데요, 중첩된 객체나 배열도 재귀적으로 병합돼요.
- `source``target`의 프로퍼티가 모두 객체 또는 배열이라면, 두 객체와 배열은 병합돼요.
- 만약에 `source`의 프로퍼티가 `undefined` 라면, `target`의 프로퍼티를 덮어씌우지 않아요.
[merge](./merge.md)와 다르게, 이 함수는 `target` 객체를 수정하지 않아요.
## 인터페이스
```typescript
function toMerged<T, S>(target: T, source: S): T & S;
```
### 파라미터
- `target` (`T`): `source` 객체가 가지고 있는 프로퍼티를 병합할 객체.
- `source` (`S`): `target` 객체로 프로퍼티를 병합할 객체.
### 반환 값
(`T & S`): `source``target` 객체가 가지고 있는 프로퍼티가 병합된 새로운 객체
## 예시
```typescript
const target = { a: 1, b: { x: 1, y: 2 } };
const source = { b: { y: 3, z: 4 }, c: 5 };
const result = toMerged(target, source);
console.log(result);
// 반환 값: { a: 1, b: { x: 1, y: 3, z: 4 }, c: 5 }
const target = { a: [1, 2], b: { x: 1 } };
const source = { a: [3], b: { y: 2 } };
const result = toMerged(target, source);
console.log(result);
// 반환 값: { a: [3, 2], b: { x: 1, y: 2 } }
const target = { a: null };
const source = { a: [1, 2, 3] };
const result = toMerged(target, source);
console.log(result);
// 반환 값: { a: [1, 2, 3] }
```
## 데모
::: sandpack
```ts index.ts
import { toMerged } from 'es-toolkit';
const target = { a: 1, b: { x: 1, y: 2 } };
const source = { b: { y: 3, z: 4 }, c: 5 };
const result = toMerged(target, source);
console.log(result);
```
:::

View File

@ -0,0 +1,62 @@
# merge
Merges the properties of the source object into the target object.
This function performs a deep merge, meaning nested objects and arrays are merged recursively.
- 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.
Unlike [toMerged](./toMerged.md), this function mutates the target object.
## Signature
```typescript
function merge<T, S>(target: T, source: S): 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.
### Returns
(`T & S`): The updated target object with properties from the source object merged in.
## Examples
```typescript
const target = { a: 1, b: { x: 1, y: 2 } };
const source = { b: { y: 3, z: 4 }, c: 5 };
const result = merge(target, source);
console.log(result);
// Output: { a: 1, b: { x: 1, y: 3, z: 4 }, c: 5 }
const target = { a: [1, 2], b: { x: 1 } };
const source = { a: [3], b: { y: 2 } };
const result = merge(target, source);
console.log(result);
// Output: { a: [3, 2], b: { x: 1, y: 2 } }
const target = { a: null };
const source = { a: [1, 2, 3] };
const result = merge(target, source);
console.log(result);
// Output: { a: [1, 2, 3] }
```
## Demo
::: sandpack
```ts index.ts
import { merge } from 'es-toolkit';
const target = { a: 1, b: { x: 1, y: 2 } };
const source = { b: { y: 3, z: 4 }, c: 5 };
const result = merge(target, source);
console.log(result);
```
:::

View File

@ -0,0 +1,62 @@
# toMerged
Merges the properties of the source object into the target object.
This function performs a deep merge, meaning nested objects and arrays are merged recursively.
- 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.
Unlike [merge](./merge.md), this function does not mutate the target object.
## Signature
```typescript
function toMerged<T, S>(target: T, source: S): T & S;
```
### Parameters
- `target` (`T`): The target object into which the source object properties will be merged.
- `source` (`S`): 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.
## Examples
```typescript
const target = { a: 1, b: { x: 1, y: 2 } };
const source = { b: { y: 3, z: 4 }, c: 5 };
const result = toMerged(target, source);
console.log(result);
// Output: { a: 1, b: { x: 1, y: 3, z: 4 }, c: 5 }
const target = { a: [1, 2], b: { x: 1 } };
const source = { a: [3], b: { y: 2 } };
const result = toMerged(target, source);
console.log(result);
// Output: { a: [3, 2], b: { x: 1, y: 2 } }
const target = { a: null };
const source = { a: [1, 2, 3] };
const result = toMerged(target, source);
console.log(result);
// Output: { a: [1, 2, 3] }
```
## Demo
::: sandpack
```ts index.ts
import { toMerged } from 'es-toolkit';
const target = { a: 1, b: { x: 1, y: 2 } };
const source = { b: { y: 3, z: 4 }, c: 5 };
const result = toMerged(target, source);
console.log(result);
```
:::

View File

@ -0,0 +1,62 @@
# merge
将源对象的属性合并到目标对象中。
此函数执行深度合并,意味着嵌套的对象和数组会递归地合并。
- 如果源对象中的属性是数组或对象,而目标对象中的相应属性也是数组或对象,它们将被合并。
- 如果源对象中的属性是未定义的,它不会覆盖目标对象中已定义的属性。
与 [toMerged](./toMerged.md) 不同,此函数会修改目标对象。
## 签名
```typescript
function merge<T, S>(target: T, source: S): T & S;
```
### 参数
- `target` (`T`): 目标对象,源对象的属性将被合并到这个对象中。这个对象会被原地修改。
- `source` (`S`): 源对象,其属性将被合并到目标对象中。
### 返回值
(`T & S`): 更新后的目标对象,其中包含了源对象的合并属性。
## 示例
```typescript
const target = { a: 1, b: { x: 1, y: 2 } };
const source = { b: { y: 3, z: 4 }, c: 5 };
const result = merge(target, source);
console.log(result);
// 输出: { a: 1, b: { x: 1, y: 3, z: 4 }, c: 5 }
const target = { a: [1, 2], b: { x: 1 } };
const source = { a: [3], b: { y: 2 } };
const result = merge(target, source);
console.log(result);
// 输出: { a: [3, 2], b: { x: 1, y: 2 } }
const target = { a: null };
const source = { a: [1, 2, 3] };
const result = merge(target, source);
console.log(result);
// 输出: { a: [1, 2, 3] }
```
## 演示
::: sandpack
```ts index.ts
import { merge } from 'es-toolkit';
const target = { a: 1, b: { x: 1, y: 2 } };
const source = { b: { y: 3, z: 4 }, c: 5 };
const result = merge(target, source);
console.log(result);
```
:::

View File

@ -0,0 +1,62 @@
# toMerged
将源对象的属性合并到目标对象中。
此函数执行深度合并,意味着嵌套的对象和数组会递归地合并。
- 如果源对象中的属性是数组或对象,而目标对象中的相应属性也是数组或对象,它们将被合并。
- 如果源对象中的属性是未定义的,它不会覆盖目标对象中已定义的属性。
与 [merge](./merge.md) 不同,这个函数不会修改目标对象。
## 签名
```typescript
function toMerged<T, S>(target: T, source: S): T & S;
```
### 参数
- `target` (`T`): 目标对象,源对象的属性将被合并到这个对象中。
- `source` (`S`): 源对象,其属性将被合并到目标对象中。
### 返回值
(`T & S`): 更新后的目标对象,其中包含了源对象的合并属性。
## 示例
```typescript
const target = { a: 1, b: { x: 1, y: 2 } };
const source = { b: { y: 3, z: 4 }, c: 5 };
const result = toMerged(target, source);
console.log(result);
// 输出: { a: 1, b: { x: 1, y: 3, z: 4 }, c: 5 }
const target = { a: [1, 2], b: { x: 1 } };
const source = { a: [3], b: { y: 2 } };
const result = toMerged(target, source);
console.log(result);
// 输出: { a: [3, 2], b: { x: 1, y: 2 } }
const target = { a: null };
const source = { a: [1, 2, 3] };
const result = toMerged(target, source);
console.log(result);
// 输出: { a: [1, 2, 3] }
```
## 演示
::: sandpack
```ts index.ts
import { toMerged } from 'es-toolkit';
const target = { a: 1, b: { x: 1, y: 2 } };
const source = { b: { y: 3, z: 4 }, c: 5 };
const result = toMerged(target, source);
console.log(result);
```
:::

View File

@ -43,6 +43,7 @@ export { set } from './object/set.ts';
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 { isPlainObject } from './predicate/isPlainObject.ts';
export { isArray } from './predicate/isArray.ts';

View File

@ -0,0 +1,354 @@
import { describe, expect, it } from 'vitest';
import { merge } from './merge';
import { args } from '../_internal/args';
import { isArguments } from '../predicate/isArguments';
import { typedArrays } from '../_internal/typedArrays';
import { range } from '../../math/range';
import { stubTrue } from '../_internal/stubTrue';
import { isEqual } from '../../predicate/isEqual';
describe('merge', () => {
it('should merge `source` into `object`', () => {
const names = {
characters: [{ name: 'barney' }, { name: 'fred' }],
};
const ages = {
characters: [{ age: 36 }, { age: 40 }],
};
const heights = {
characters: [{ height: '5\'4"' }, { height: '5\'5"' }],
};
const expected = {
characters: [
{ name: 'barney', age: 36, height: '5\'4"' },
{ name: 'fred', age: 40, height: '5\'5"' },
],
};
expect(merge(names, ages, heights)).toEqual(expected);
});
it('should merge sources containing circular references', () => {
const object: any = {
foo: { a: 1 },
bar: { a: 2 },
};
const source: any = {
foo: { b: { c: { d: {} } } },
bar: {},
};
source.foo.b.c.d = source;
source.bar.b = source.foo.b;
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);
});
it('should work with four arguments', () => {
const expected = { a: 4 };
const actual = merge({ a: 1 }, { a: 2 }, { a: 3 }, expected);
expect(actual).toEqual(expected);
});
it('should merge onto function `object` values', () => {
function Foo() {}
const source = { a: 1 };
const actual = merge(Foo, source);
expect(actual).toBe(Foo);
// eslint-disable-next-line
// @ts-ignore
expect(Foo.a).toBe(1);
});
it('should treat sparse array sources as dense', () => {
var array = [1];
array[2] = 3;
var actual = merge([], array),
expected: any = array.slice();
expected[1] = undefined;
expect('1' in actual).toBe(true);
expect(actual).toEqual(expected);
});
it('should merge first source object properties to function', () => {
const fn = function () {};
const object = { prop: {} };
const actual = merge({ prop: fn }, object);
expect(actual).toEqual(object);
});
it('should merge first and second source object properties to function', () => {
const fn = function () {};
const object = { prop: {} };
const actual = merge({ prop: fn }, { prop: fn }, object);
expect(actual).toEqual(object);
});
it('should not merge onto function values of sources', () => {
const source1 = { a: function () {} };
const source2 = { a: { b: 2 } };
const expected = { a: { b: 2 } };
let actual = merge({}, source1, source2);
expect(actual).toEqual(expected);
expect('b' in source1.a).toBe(false);
actual = merge(source1, source2);
expect(actual).toEqual(expected);
});
it('should merge onto non-plain `object` values', () => {
function Foo() {}
// eslint-disable-next-line
// @ts-ignore
const object = new Foo();
const actual = merge(object, { a: 1 });
expect(actual).toBe(object);
expect(object.a).toBe(1);
});
it('should merge `arguments` objects', () => {
const object1 = { value: args };
const object2 = { value: { 3: 4 } };
let expected: any = { 0: 1, 1: 2, 2: 3, 3: 4 };
let actual: any = merge(object1, object2);
expect('3' in args).toBe(false);
expect(isArguments(actual.value)).toBe(false);
expect(actual.value).toEqual(expected);
object1.value = args;
actual = merge(object2, object1);
expect(isArguments(actual.value)).toBe(false);
expect(actual.value).toEqual(expected);
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);
});
it('should merge typed arrays', () => {
const array1 = [0];
const array2 = [0, 0];
const array3 = [0, 0, 0, 0];
const array4 = [0, 0, 0, 0, 0, 0, 0, 0];
const arrays = [array2, array1, array4, array3, array2, array4, array4, array3, array2];
const buffer = new ArrayBuffer(8);
let expected = typedArrays.map((type, index) => {
const array = arrays[index].slice();
array[0] = 1;
// eslint-disable-next-line
// @ts-ignore
return globalThis[type] ? { value: array } : false;
});
let actual = typedArrays.map(type => {
// eslint-disable-next-line
// @ts-ignore
const Ctor = globalThis[type];
return Ctor ? merge({ value: new Ctor(buffer) }, { value: [1] }) : false;
});
expect(Array.isArray(actual)).toBe(true);
expect(actual).toEqual(expected);
expected = typedArrays.map((type, index) => {
const array = arrays[index].slice();
array.push(1);
// eslint-disable-next-line
// @ts-ignore
return globalThis[type] ? { value: array } : false;
});
actual = typedArrays.map((type, index) => {
// eslint-disable-next-line
// @ts-ignore
const Ctor = globalThis[type];
const array = range(arrays[index].length);
array.push(1);
return Ctor ? merge({ value: array }, { value: new Ctor(buffer) }) : false;
});
expect(Array.isArray(actual)).toBe(true);
expect(actual).toEqual(expected);
});
it('should assign `null` values', () => {
const actual = merge({ a: 1 }, { a: null });
// eslint-disable-next-line
// @ts-ignore
expect(actual.a).toBe(null);
});
it('should assign non array/buffer/typed-array/plain-object source values directly', () => {
function Foo() {}
// eslint-disable-next-line
// @ts-ignore
const values = [new Foo(), new Boolean(), new Date(), Foo, new Number(), new String(), new RegExp()];
const expected = values.map(stubTrue);
const actual = values.map(value => {
const object = merge({}, { a: value, b: { c: value } });
return object.a === value && object.b.c === value;
});
expect(actual).toEqual(expected);
});
it('should clone buffer source values', () => {
const buffer = Buffer.from([1]);
const actual = merge({}, { value: buffer }).value;
expect(Buffer.isBuffer(actual)).toBe(true);
expect(actual[0]).toBe(buffer[0]);
expect(actual).not.toBe(buffer);
});
it('should deep clone array/typed-array/plain-object source values', () => {
const typedArray = Uint8Array ? new Uint8Array([1]) : { buffer: [1] };
const props = ['0', 'buffer', 'a'];
const values: any = [[{ a: 1 }], typedArray, { a: [1] }];
const expected = values.map(stubTrue);
const actual = values.map((value: any, index: any) => {
const key = props[index];
const object = merge({}, { value: value });
const subValue = value[key];
const newValue = object.value;
const newSubValue = newValue[key];
return newValue !== value && newSubValue !== subValue && isEqual(newValue, value);
});
expect(actual).toEqual(expected);
});
it('should not augment source objects', () => {
var source1: any = { a: [{ a: 1 }] };
var source2: any = { a: [{ b: 2 }] };
var actual = merge({}, source1, source2);
expect(source1.a).toEqual([{ a: 1 }]);
expect(source2.a).toEqual([{ b: 2 }]);
expect(actual.a).toEqual([{ a: 1, b: 2 }]);
source1 = { a: [[1, 2, 3]] };
source2 = { a: [[3, 4]] };
var actual = merge({}, source1, source2);
expect(source1.a).toEqual([[1, 2, 3]]);
expect(source2.a).toEqual([[3, 4]]);
expect(actual.a).toEqual([[3, 4, 3]]);
});
it('should merge plain objects onto non-plain objects', () => {
function Foo(object: any) {
// eslint-disable-next-line
// @ts-ignore
Object.assign(this, object);
}
const object = { a: 1 };
// eslint-disable-next-line
// @ts-ignore
let actual = merge(new Foo(), object);
expect(actual instanceof Foo);
// eslint-disable-next-line
// @ts-ignore
expect(actual).toEqual(new Foo(object));
// eslint-disable-next-line
// @ts-ignore
actual = merge([new Foo()], [object]);
expect(actual[0] instanceof Foo);
// eslint-disable-next-line
// @ts-ignore
expect(actual).toEqual([new Foo(object)]);
});
it('should not overwrite existing values with `undefined` values of object sources', () => {
const actual = merge({ a: 1 }, { a: undefined, b: undefined });
expect(actual).toEqual({ a: 1, b: undefined });
});
it('should not overwrite existing values with `undefined` values of array sources', () => {
let array: any = [1];
array[2] = 3;
let actual = merge([4, 5, 6], array);
const expected = [1, 5, 3];
expect(actual).toEqual(expected);
array = [1, , 3];
array[1] = undefined;
actual = merge([4, 5, 6], array);
expect(actual).toEqual(expected);
});
it('should skip merging when `object` and `source` are the same value', () => {
const object = {};
let pass = true;
Object.defineProperty(object, 'a', {
configurable: true,
enumerable: true,
get: function () {
pass = false;
},
set: function () {
pass = false;
},
});
merge(object, object);
expect(pass);
});
it('should convert values to arrays when merging arrays of `source`', () => {
const object = { a: { 1: 'y', b: 'z', length: 2 } };
let actual: any = merge(object, { a: ['x'] });
expect(actual).toEqual({ a: ['x', 'y'] });
actual = merge({ a: {} }, { a: [] });
expect(actual).toEqual({ a: [] });
});
it('should convert strings to arrays when merging arrays of `source`', () => {
const object = { a: 'abcde' };
const actual = merge(object, { a: ['x', 'y', 'z'] });
expect(actual).toEqual({ a: ['x', 'y', 'z'] });
});
});

309
src/compat/object/merge.ts Normal file
View File

@ -0,0 +1,309 @@
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.
*
* 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 {S} source - The first source object whose properties will be merged into the target object.
* @returns {O & S} The updated target object with properties from the source object(s) merged in.
*
* @template O - Type of the target object.
* @template S - Type of the first source object.
*
* @example
* const target = { a: 1, b: { x: 1, y: 2 } };
* const source = { b: { y: 3, z: 4 }, c: 5 };
*
* const result = merge(target, source);
* console.log(result);
* // Output: { a: 1, b: { x: 1, y: 3, z: 4 }, c: 5 }
*
* @example
* const target = { a: [1, 2], b: { x: 1 } };
* const source = { a: [3], b: { y: 2 } };
*
* const result = merge(target, source);
* console.log(result);
* // Output: { a: [3], b: { x: 1, y: 2 } }
*
* @example
* const target = { a: null };
* const source = { a: [1, 2, 3] };
*
* const result = merge(target, source);
* console.log(result);
* // Output: { a: [1, 2, 3] }
*/
export function merge<O, S>(object: O, source: S): O & 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.
*
* 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: { x: 1, y: 2 } };
* const source = { b: { y: 3, z: 4 }, c: 5 };
*
* const result = merge(target, source);
* console.log(result);
* // Output: { a: 1, b: { x: 1, y: 3, z: 4 }, c: 5 }
*
* @example
* const target = { a: [1, 2], b: { x: 1 } };
* const source = { a: [3], b: { y: 2 } };
*
* const result = merge(target, source);
* console.log(result);
* // Output: { a: [3], b: { x: 1, y: 2 } }
*
* @example
* const target = { a: null };
* const source = { a: [1, 2, 3] };
*
* const result = merge(target, source);
* console.log(result);
* // Output: { a: [1, 2, 3] }
*/
export function merge<O, S1, S2>(object: O, source1: S1, source2: S2): 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.
*
* 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: { x: 1, y: 2 } };
* const source = { b: { y: 3, z: 4 }, c: 5 };
*
* const result = merge(target, source);
* console.log(result);
* // Output: { a: 1, b: { x: 1, y: 3, z: 4 }, c: 5 }
*
* @example
* const target = { a: [1, 2], b: { x: 1 } };
* const source = { a: [3], b: { y: 2 } };
*
* const result = merge(target, source);
* console.log(result);
* // Output: { a: [3], b: { x: 1, y: 2 } }
*
* @example
* const target = { a: null };
* const source = { a: [1, 2, 3] };
*
* const result = merge(target, source);
* console.log(result);
* // Output: { a: [1, 2, 3] }
*/
export function merge<O, S1, S2, S3>(object: O, source1: S1, source2: S2, source3: S3): 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.
*
* 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: { x: 1, y: 2 } };
* const source = { b: { y: 3, z: 4 }, c: 5 };
*
* const result = merge(target, source);
* console.log(result);
* // Output: { a: 1, b: { x: 1, y: 3, z: 4 }, c: 5 }
*
* @example
* const target = { a: [1, 2], b: { x: 1 } };
* const source = { a: [3], b: { y: 2 } };
*
* const result = merge(target, source);
* console.log(result);
* // Output: { a: [3], b: { x: 1, y: 2 } }
*
* @example
* const target = { a: null };
* const source = { a: [1, 2, 3] };
*
* const result = merge(target, source);
* console.log(result);
* // Output: { a: [1, 2, 3] }
*/
export function merge<O, S1, S2, S3, S4>(
object: O,
source1: S1,
source2: S2,
source3: S3,
source4: S4
): 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.
*
* 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: { x: 1, y: 2 } };
* const source = { b: { y: 3, z: 4 }, c: 5 };
*
* const result = merge(target, source);
* console.log(result);
* // Output: { a: 1, b: { x: 1, y: 3, z: 4 }, c: 5 }
*
* @example
* const target = { a: [1, 2], b: { x: 1 } };
* const source = { a: [3], b: { y: 2 } };
*
* const result = merge(target, source);
* console.log(result);
* // Output: { a: [3], b: { x: 1, y: 2 } }
*
* @example
* const target = { a: null };
* const source = { a: [1, 2, 3] };
*
* const result = merge(target, source);
* console.log(result);
* // Output: { a: [1, 2, 3] }
*/
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;
}

View File

@ -33,7 +33,7 @@ export function isPlainObject(object?: unknown): boolean {
return true;
}
if (object.toString() !== '[object Object]') {
if (Object.prototype.toString.call(object) !== '[object Object]') {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const tag = object[Symbol.toStringTag];

View File

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

77
src/object/merge.spec.ts Normal file
View File

@ -0,0 +1,77 @@
import { describe, it, expect } from 'vitest';
import { merge } from './merge';
describe('merge', () => {
it('should merge properties from source object into target object', () => {
const target = { a: 1, b: 2 };
const source = { b: 3, c: 4 };
const result = merge(target, source);
expect(result).toEqual({ a: 1, b: 3, c: 4 });
});
it('should deeply merge nested objects', () => {
const target = { a: { x: 1, y: 2 }, b: 2 };
const source = { a: { y: 3, z: 4 }, c: 5 };
const result = merge(target, source);
expect(result).toEqual({ a: { x: 1, y: 3, z: 4 }, b: 2, c: 5 });
const names = {
characters: [{ name: 'barney' }, { name: 'fred' }],
};
const ages = {
characters: [{ age: 36 }, { age: 40 }],
};
const heights = {
characters: [{ height: '5\'4"' }, { height: '5\'5"' }],
};
const expected = {
characters: [
{ name: 'barney', age: 36, height: '5\'4"' },
{ name: 'fred', age: 40, height: '5\'5"' },
],
};
expect(merge(merge(names, ages), heights)).toEqual(expected);
const target2 = { a: [1, 2], b: { x: 1 } };
const source2 = { a: [3], b: { y: 2 } };
expect(merge(target2, source2)).toEqual({ a: [3, 2], b: { x: 1, y: 2 } });
});
it('should merge arrays deeply', () => {
const target = { a: [1, 2] };
const source = { a: [3, 4] };
const result = merge(target, source);
expect(result).toEqual({ a: [3, 4] });
});
it('should handle merging with null values', () => {
const target = { a: null };
const source = { a: [1, 2, 3] };
const result = merge(target, source);
expect(result).toEqual({ a: [1, 2, 3] });
});
it('should not overwrite existing values with undefined from source', () => {
const target = { a: 1, b: 2 };
const source = { b: undefined, c: 3 };
const result = merge(target, source);
expect(result).toEqual({ a: 1, b: 2, c: 3 });
});
it('should handle merging of deeply nested objects with arrays and objects', () => {
const target = { a: { b: { c: [1] } } };
const source = { a: { b: { c: [2], d: 3 }, e: [4] } };
const result = merge(target, source);
expect(result).toEqual({ a: { b: { c: [2], d: 3 }, e: [4] } });
});
});

61
src/object/merge.ts Normal file
View File

@ -0,0 +1,61 @@
import { isObjectLike } from '../compat/predicate/isObjectLike.ts';
/**
* Merges the properties of the source object into the target object.
*
* This function performs a deep merge, meaning nested objects and arrays are merged recursively.
* 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.
*
* @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.
*
* @template T - Type of the target object.
* @template S - Type of the source object.
*
* @example
* const target = { a: 1, b: { x: 1, y: 2 } };
* const source = { b: { y: 3, z: 4 }, c: 5 };
*
* const result = merge(target, source);
* console.log(result);
* // Output: { a: 1, b: { x: 1, y: 3, z: 4 }, c: 5 }
*
* @example
* const target = { a: [1, 2], b: { x: 1 } };
* const source = { a: [3], b: { y: 2 } };
*
* const result = merge(target, source);
* console.log(result);
* // Output: { a: [3, 2], b: { x: 1, y: 2 } }
*
* @example
* const target = { a: null };
* const source = { a: [1, 2, 3] };
*
* const result = merge(target, source);
* console.log(result);
* // Output: { a: [1, 2, 3] }
*/
export function merge<T, S>(target: T, source: S): T & S;
export function merge(target: any, source: 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];
if (Array.isArray(sourceValue)) {
target[key] = merge(targetValue ?? [], sourceValue);
} else if (isObjectLike(targetValue) && isObjectLike(sourceValue)) {
target[key] = merge(targetValue ?? {}, sourceValue);
} else if (targetValue === undefined || sourceValue !== undefined) {
target[key] = sourceValue;
}
}
return target;
}

View File

@ -0,0 +1,87 @@
import { describe, it, expect } from 'vitest';
import { toMerged } from './toMerged';
describe('toMerged', () => {
it('should merge properties from source object into target object', () => {
const target = { a: 1, b: 2 };
const source = { b: 3, c: 4 };
const result = toMerged(target, source);
expect(result).toEqual({ a: 1, b: 3, c: 4 });
expect(target).toEqual({ a: 1, b: 2 });
});
it('should deeply merge nested objects', () => {
const target = { a: { x: 1, y: 2 }, b: 2 };
const source = { a: { y: 3, z: 4 }, c: 5 };
const result = toMerged(target, source);
expect(result).toEqual({ a: { x: 1, y: 3, z: 4 }, b: 2, c: 5 });
expect(target).toEqual({ a: { x: 1, y: 2 }, b: 2 });
const names = {
characters: [{ name: 'barney' }, { name: 'fred' }],
};
const ages = {
characters: [{ age: 36 }, { age: 40 }],
};
const heights = {
characters: [{ height: '5\'4"' }, { height: '5\'5"' }],
};
const expected = {
characters: [
{ name: 'barney', age: 36, height: '5\'4"' },
{ name: 'fred', age: 40, height: '5\'5"' },
],
};
expect(toMerged(toMerged(names, ages), heights)).toEqual(expected);
expect(names).toEqual({
characters: [{ name: 'barney' }, { name: 'fred' }],
});
const target2 = { a: [1, 2], b: { x: 1 } };
const source2 = { a: [3], b: { y: 2 } };
expect(toMerged(target2, source2)).toEqual({ a: [3, 2], b: { x: 1, y: 2 } });
expect(target2).toEqual({ a: [1, 2], b: { x: 1 } });
});
it('should merge arrays deeply', () => {
const target = { a: [1, 2] };
const source = { a: [3, 4] };
const result = toMerged(target, source);
expect(result).toEqual({ a: [3, 4] });
expect(target).toEqual({ a: [1, 2] });
});
it('should handle merging with null values', () => {
const target = { a: null };
const source = { a: [1, 2, 3] };
const result = toMerged(target, source);
expect(result).toEqual({ a: [1, 2, 3] });
expect(target).toEqual({ a: null });
});
it('should not overwrite existing values with undefined from source', () => {
const target = { a: 1, b: 2 };
const source = { b: undefined, c: 3 };
const result = toMerged(target, source);
expect(result).toEqual({ a: 1, b: 2, c: 3 });
expect(target).toEqual({ a: 1, b: 2 });
});
it('should handle merging of deeply nested objects with arrays and objects', () => {
const target = { a: { b: { c: [1] } } };
const source = { a: { b: { c: [2], d: 3 }, e: [4] } };
const result = toMerged(target, source);
expect(result).toEqual({ a: { b: { c: [2], d: 3 }, e: [4] } });
expect(target).toEqual({ a: { b: { c: [1] } } });
});
});

47
src/object/toMerged.ts Normal file
View File

@ -0,0 +1,47 @@
import { cloneDeep } from './cloneDeep.ts';
import { merge } from './merge.ts';
/**
* Merges the properties of the source object into a deep clone of the target object.
* Unlike `merge`, This function does not modify the original target object.
*
* This function performs a deep merge, meaning nested objects and arrays are merged recursively.
*
* - 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.
*
* @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.
*
* @template T - Type of the target object.
* @template S - Type of the source object.
*
* @example
* const target = { a: 1, b: { x: 1, y: 2 } };
* const source = { b: { y: 3, z: 4 }, c: 5 };
*
* const result = toMerged(target, source);
* console.log(result);
* // Output: { a: 1, b: { x: 1, y: 3, z: 4 }, c: 5 }
*
* @example
* const target = { a: [1, 2], b: { x: 1 } };
* const source = { a: [3], b: { y: 2 } };
*
* const result = toMerged(target, source);
* console.log(result);
* // Output: { a: [3, 2], b: { x: 1, y: 2 } }
*
* @example
* const target = { a: null };
* const source = { a: [1, 2, 3] };
*
* const result = toMerged(target, source);
* console.log(result);
* // Output: { a: [1, 2, 3] }
*/
export function toMerged<T, S>(target: T, source: S): T & S;
export function toMerged(target: any, source: any) {
return merge(cloneDeep(target), source);
}