Skip to content
Open

feat #19

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,40 @@ const omitted = $omit(user, ["email"]); // omitted = { id: 1, name: 'John Doe' }
console.log(omitted.email); // Output: undefined
```

```ts
import { $deepMerge } from "@uchihori/utilities/object";

const target = {
a: 1,
b: {
c: "hello",
d: [1, 2],
},
};

const source = {
b: {
d: [3, 4],
e: true,
},
f: "world",
};

const merged = $deepMerge(target, source);
console.log(merged);
/* Output:
{
a: 1,
b: {
c: "hello",
d: [1, 2, 3, 4],
e: true
},
f: "world"
}
*/
```

## array

```ts
Expand Down Expand Up @@ -367,3 +401,36 @@ import type { FromPairs } from "./types.ts";

type ObjectFromPairs = FromPairs<[["a", 1], ["b", 2]]>; // { a: 1, b: 2 }
```

```ts
import type { DeepMerge } from "./types.ts";

type A = {
a: number;
b: {
c: string;
d: number[];
};
};

type B = {
b: {
d: boolean[];
e: boolean;
};
f: string;
};

type Merged = DeepMerge<A, B>;
/* Result:
{
a: number;
b: {
c: string;
d: (number | boolean)[];
e: boolean;
};
f: string;
}
*/
```
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@
"@std/assert": "jsr:@std/assert@^0.226.0",
"@std/testing": "jsr:@std/testing@^0.225.3"
},
"version": "0.5.0"
"version": "0.6.0"
}
68 changes: 67 additions & 1 deletion object.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { assertEquals } from "@std/assert/assert-equals";
import { assertThrows } from "@std/assert/assert-throws";
import { $omit, $pick } from "./object.ts";
import { $deepMerge, $omit, $pick } from "./object.ts";

Deno.test("$pick", () => {
const obj = { a: "1", b: 2 };
Expand Down Expand Up @@ -172,3 +172,69 @@ Deno.test("$omit with invalid arguments", () => {
"expected an array for a second argument",
);
});

Deno.test("$deepMerge", () => {
// 基本的なオブジェクトのマージ
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const merged1 = $deepMerge(obj1, obj2);
assertEquals(merged1, { a: 1, b: 3, c: 4 });

// ネストされたオブジェクトのマージ
const nested1 = { a: 1, b: { c: 2, d: 3 } };
const nested2 = { b: { d: 4, e: 5 }, f: 6 };
const mergedNested = $deepMerge(nested1, nested2);
assertEquals(mergedNested, { a: 1, b: { c: 2, d: 4, e: 5 }, f: 6 });

// 配列のマージ
const withArray1 = { a: 1, b: [1, 2, 3] };
const withArray2 = { b: [4, 5], c: 3 };
const mergedArray = $deepMerge(withArray1, withArray2);
assertEquals(mergedArray, { a: 1, b: [1, 2, 3, 4, 5], c: 3 });

// ネストされた複雑なオブジェクトのマージ
const complex1 = { a: 1, b: { c: 2, d: { e: 3, f: 4 } } };
const complex2 = { b: { d: { f: 5, g: 6 }, h: 7 } };
const mergedComplex = $deepMerge(complex1, complex2);
assertEquals(mergedComplex, {
a: 1,
b: { c: 2, d: { e: 3, f: 5, g: 6 }, h: 7 },
});

// 配列を含むネストされたオブジェクトのマージ
const arrayNested1 = { a: { b: [1, 2], c: 3 } };
const arrayNested2 = { a: { b: [4, 5], d: 6 } };
const mergedArrayNested = $deepMerge(arrayNested1, arrayNested2);
assertEquals(mergedArrayNested, { a: { b: [1, 2, 4, 5], c: 3, d: 6 } });
});

Deno.test("$deepMerge with invalid arguments", () => {
// 非オブジェクト引数に対するエラーハンドリング
assertThrows(
// deno-lint-ignore ban-ts-comment
// @ts-ignore
() => $deepMerge("not an object", {}),
"expected a non-null object for the first argument",
);

assertThrows(
// deno-lint-ignore ban-ts-comment
// @ts-ignore
() => $deepMerge({}, "not an object"),
"expected a non-null object for the second argument",
);

assertThrows(
// deno-lint-ignore ban-ts-comment
// @ts-ignore
() => $deepMerge(null, {}),
"expected a non-null object for the first argument",
);

assertThrows(
// deno-lint-ignore ban-ts-comment
// @ts-ignore
() => $deepMerge({}, null),
"expected a non-null object for the second argument",
);
});
74 changes: 74 additions & 0 deletions object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,77 @@ export const $omit = <T extends object, E extends keyof T>(
}
return result;
};

import type { DeepMerge } from "./types.ts";

/**
* Deeply merges two or more objects.
*
* @template T - The type of the first object.
* @template U - The type of the second object.
* @param {T} target - The first object to merge.
* @param {U} source - The second object to merge.
* @returns {DeepMerge<T, U>} A new object with all properties deeply merged.
* @throws Will throw an error if any argument is not a non-null object.
*/
export const $deepMerge = <T extends object, U extends object>(
target: T,
source: U,
): DeepMerge<T, U> => {
// 基本的な検証
if (typeof target !== "object" || target === null) {
throw new Error("expected a non-null object for the first argument");
}
if (typeof source !== "object" || source === null) {
throw new Error("expected a non-null object for the second argument");
}

// 2つのオブジェクトをマージする内部関数
const merge = <A extends object, B extends object>(
target: A,
source: B,
): DeepMerge<A, B> => {
// 結果オブジェクトを作成
const result = { ...target } as Record<PropertyKey, unknown>;

// ソースオブジェクトのプロパティをマージ
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
const sourceValue = source[key as keyof B];
const targetValue = key in target
? (target as Record<string, unknown>)[key]
: undefined;

if (
targetValue !== null &&
targetValue !== undefined &&
sourceValue !== null &&
sourceValue !== undefined &&
typeof targetValue === "object" &&
typeof sourceValue === "object"
) {
if (Array.isArray(targetValue) && Array.isArray(sourceValue)) {
// 両方が配列の場合はマージ(連結)
result[key] = [...targetValue, ...sourceValue] as unknown;
} else if (
!Array.isArray(targetValue) &&
!Array.isArray(sourceValue)
) {
// 両方がオブジェクト(配列でない)の場合は再帰的にマージ
result[key] = merge(targetValue as object, sourceValue as object);
} else {
// 一方が配列で一方がオブジェクトの場合はソースの値で上書き
result[key] = sourceValue;
}
} else {
// それ以外の場合はソースの値で上書き
result[key] = sourceValue;
}
}
}

return result as DeepMerge<A, B>;
};

return merge(target, source);
};
48 changes: 48 additions & 0 deletions type.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { AssertTrue, IsExact } from "@std/testing/types";
import type {
DeepMerge,
FillArray,
FromPairs,
IsTuple,
Expand Down Expand Up @@ -139,3 +140,50 @@ Deno.test("RemoveItems", () => {
IsExact<RemoveItems<(number | string)[], 1 | 2 | 3>, (number | string)[]>
>;
});

Deno.test("DeepMerge", () => {
// 基本的なオブジェクトのマージ
type A = { a: number; b: string };
type B = { b: boolean; c: number };
type ExpectedAB = { a: number; b: boolean; c: number };
type _1 = AssertTrue<IsExact<DeepMerge<A, B>, ExpectedAB>>;

// ネストされたオブジェクトのマージ
type C = { a: number; b: { c: string; d: number } };
type D = { b: { d: boolean; e: string }; f: number };
type ExpectedCD = {
a: number;
b: { c: string; d: boolean; e: string };
f: number;
};
type _2 = AssertTrue<IsExact<DeepMerge<C, D>, ExpectedCD>>;

// 配列のマージ
type E = { a: number; b: string[]; c: { d: number[] } };
type F = { b: boolean[]; c: { d: string[] } };
type ExpectedEF = {
a: number;
b: Array<string | boolean>;
c: { d: Array<number | string> };
};
type _3 = AssertTrue<IsExact<DeepMerge<E, F>, ExpectedEF>>;

// プリミティブ値の上書き
type G = { a: number; b: string };
type H = { a: string };
type ExpectedGH = { a: string; b: string };
type _4 = AssertTrue<IsExact<DeepMerge<G, H>, ExpectedGH>>;

// 複雑なネストされたオブジェクトのマージ
type I = { a: { b: { c: number; d: string }; e: boolean }; f: number[] };
type J = { a: { b: { d: number; g: boolean }; h: string }; f: string[] };
type ExpectedIJ = {
a: {
b: { c: number; d: number; g: boolean };
e: boolean;
h: string;
};
f: Array<number | string>;
};
type _5 = AssertTrue<IsExact<DeepMerge<I, J>, ExpectedIJ>>;
});
37 changes: 37 additions & 0 deletions types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,40 @@ export type RemoveItems<T extends Primitive[], U extends Primitive> =
* Represents primitive types in TypeScript.
*/
export type Primitive = string | number | boolean | symbol | null | undefined;

/**
* Deeply merges two types, recursively combining their properties.
*
* @template T - The first type to merge.
* @template U - The second type to merge.
*
* @remarks
* This type does the following:
* 1. If both T and U are objects, it recursively merges their properties.
* 2. If either T or U is not an object, U takes precedence.
* 3. For arrays, they are merged by creating a union of their element types.
*
* @example
* type A = { a: number; b: { c: string; } };
* type B = { b: { d: boolean; }; e: string; };
* type Merged = DeepMerge<A, B>;
* // { a: number; b: { c: string; d: boolean; }; e: string; }
*
* @returns A new type representing the deep merge of T and U.
*/
export type DeepMerge<T, U> = [T, U] extends [object, object]
? T extends readonly (infer T1)[]
? U extends readonly (infer U1)[] ? Array<T1 | U1> // 配列同士の場合は要素型をマージ
: U
: U extends readonly (infer U1)[] ? U
: {
[K in keyof T | keyof U]: K extends keyof U
? K extends keyof T
? T[K] extends object ? U[K] extends object ? DeepMerge<T[K], U[K]>
: U[K]
: U[K]
: U[K]
: K extends keyof T ? T[K]
: never;
}
: U;