diff --git a/benchmarks/performance/isMatch.bench.ts b/benchmarks/performance/isMatch.bench.ts new file mode 100644 index 00000000..c8f27c7b --- /dev/null +++ b/benchmarks/performance/isMatch.bench.ts @@ -0,0 +1,12 @@ +import { bench, describe } from 'vitest'; +import { isMatch as isMatchToolkit } from 'es-toolkit/compat'; +import { isMatch as isMatchLodash } from 'lodash'; + +describe('isMatch', () => { + bench('es-toolkit/isMatch', () => { + isMatchToolkit({ a: { b: { c: 1, d: 2 }, e: 3 }, f: 4 }, { a: { b: { c: 1 } } }) + }); + bench('lodash/isMatch', () => { + isMatchLodash({ a: { b: { c: 1, d: 2 }, e: 3 }, f: 4 }, { a: { b: { c: 1 } } }) + }); +}); diff --git a/benchmarks/performance/matches.bench.ts b/benchmarks/performance/matches.bench.ts new file mode 100644 index 00000000..6f0be00b --- /dev/null +++ b/benchmarks/performance/matches.bench.ts @@ -0,0 +1,14 @@ +import { bench, describe } from 'vitest'; +import { matches as matchesToolkit } from 'es-toolkit/compat'; +import { matches as matchesLodash } from 'lodash'; + +describe('matches', () => { + bench('es-toolkit/matches', () => { + const isMatch = matchesToolkit({ a: { b: { c: 1 } } }) + isMatch({ a: { b: { c: 1, d: 2 }, e: 3 }, f: 4 }); + }); + bench('lodash/matches', () => { + const isMatch = matchesLodash({ a: { b: { c: 1 } } }); + isMatch({ a: { b: { c: 1, d: 2 }, e: 3 }, f: 4 }); + }); +}); diff --git a/docs/.vitepress/en.mts b/docs/.vitepress/en.mts index ce03653f..16b8b036 100644 --- a/docs/.vitepress/en.mts +++ b/docs/.vitepress/en.mts @@ -159,6 +159,8 @@ function sidebar(): DefaultTheme.Sidebar { { text: 'isLength', link: '/reference/predicate/isLength' }, { text: 'isPlainObject', link: '/reference/predicate/isPlainObject' }, { text: 'isPrimitive', link: '/reference/predicate/isPrimitive' }, + { text: 'isMatch (compat)', link: '/reference/compat/predicate/isMatch' }, + { text: 'matches (compat)', link: '/reference/compat/predicate/matches' }, { text: 'isNil', link: '/reference/predicate/isNil' }, { text: 'isNotNil', link: '/reference/predicate/isNotNil' }, { text: 'isNull', link: '/reference/predicate/isNull' }, diff --git a/docs/.vitepress/ko.mts b/docs/.vitepress/ko.mts index 2ee37449..2a56fdf2 100644 --- a/docs/.vitepress/ko.mts +++ b/docs/.vitepress/ko.mts @@ -170,6 +170,8 @@ function sidebar(): DefaultTheme.Sidebar { { text: 'isLength', link: '/ko/reference/predicate/isLength' }, { text: 'isPlainObject', link: '/ko/reference/predicate/isPlainObject' }, { text: 'isPrimitive', link: '/ko/reference/predicate/isPrimitive' }, + { text: 'isMatch (호환성)', link: '/ko/reference/compat/predicate/isMatch' }, + { text: 'matches (호환성)', link: '/ko/reference/compat/predicate/matches' }, { text: 'isNil', link: '/ko/reference/predicate/isNil' }, { text: 'isNotNil', link: '/ko/reference/predicate/isNotNil' }, { text: 'isNull', link: '/ko/reference/predicate/isNull' }, diff --git a/docs/.vitepress/zh_hans.mts b/docs/.vitepress/zh_hans.mts index cccdca98..8db32a88 100644 --- a/docs/.vitepress/zh_hans.mts +++ b/docs/.vitepress/zh_hans.mts @@ -155,6 +155,8 @@ function sidebar(): DefaultTheme.Sidebar { { text: 'isLength', link: '/zh_hans/reference/predicate/isLength' }, { text: 'isPlainObject', link: '/zh_hans/reference/predicate/isPlainObject' }, { text: 'isPrimitive', link: '/zh_hans/reference/predicate/isPrimitive' }, + { text: 'isMatch (兼容性)', link: '/zh_hans/reference/compat/predicate/isMatch' }, + { text: 'matches (兼容性)', link: '/zh_hans/reference/compat/predicate/matches' }, { text: 'isNil', link: '/zh_hans/reference/predicate/isNil' }, { text: 'isNotNil', link: '/zh_hans/reference/predicate/isNotNil' }, { text: 'isNull', link: '/zh_hans/reference/predicate/isNull' }, diff --git a/docs/ko/reference/compat/predicate/isMatch.md b/docs/ko/reference/compat/predicate/isMatch.md new file mode 100644 index 00000000..fb81f8ef --- /dev/null +++ b/docs/ko/reference/compat/predicate/isMatch.md @@ -0,0 +1,54 @@ +# isMatch + +::: info +이 함수는 [lodash와 완전히 호환](../../../compatibility.md)돼요. `es-toolkit/compat` 라이브러리에서 쓸 수 있어요. +::: + +`target`이 `source`의 모양 및 값과 일치하는지 확인해요. 객체, 배열, `Map`, `Set`의 깊은 비교를 지원해요. + +## 인터페이스 + +```typescript +function isMatch(target: unknown, source: unknown): boolean; +``` + +## 파라미터 + +- `target` (`unknown`): 모양과 값이 일치하는지 확인할 값. +- `source` (`unknown`): 확인할 모양과 값을 가진 객체. + +## 반환 값 + +- (`boolean`): `target`이 `source`의 모양 및 값과 일치하면 `true`. 아니면 `false`. + +## 예시 + +### 객체 일치 + +```typescript +isMatch({ a: 1, b: 2 }, { a: 1 }); // true +``` + +### 배열 일치 + +```typescript +isMatch([1, 2, 3], [1, 2, 3]); // true +isMatch([1, 2, 2, 3], [2, 2]); // true +isMatch([1, 2, 3], [2, 2]); // false +``` + +### `Map` 일치 + +```typescript +const targetMap = new Map([['key1', 'value1'], ['key2', 'value2']]); +const sourceMap = new Map([['key1', 'value1']]); +isMatch(targetMap, sourceMap); // true +``` + +### `Set` 일치 + +```javascript +const targetSet = new Set([1, 2, 3]); +const sourceSet = new Set([1, 2]); +isMatch(targetSet, sourceSet); // true +``` diff --git a/docs/ko/reference/compat/predicate/matches.md b/docs/ko/reference/compat/predicate/matches.md new file mode 100644 index 00000000..e3c39ff1 --- /dev/null +++ b/docs/ko/reference/compat/predicate/matches.md @@ -0,0 +1,52 @@ +# matches + +::: info +이 함수는 [lodash와 완전히 호환](../../../compatibility.md)돼요. `es-toolkit/compat` 라이브러리에서 쓸 수 있어요. +::: + +`source`의 모양 및 값과 일치하는지 확인하는 함수를 만들어요. +객체, 배열, `Map`, `Set`과의 깊은 비교를 지원해요. + +이 함수의 동작은 [isMatch](./isMatch.md)와 동일하고, 호출하는 방법만 달라요. + +## 인터페이스 + +```typescript +function matches(source: unknown): (target: unknown) => boolean; +``` + +## 파라미터 + +- `source` (`unknown`): 확인하는 함수가 참고할 객체. + +## 반환 값 + +- (`(target: unknown) => boolean`): `source`의 모양 및 값과 일치하는지 확인하는 함수. `target`이 `source`과 일치하면 `true`, 아니면 `false`를 반환해요. + + +## 예시 + +### 객체 일치 + +```typescript +const matcher = matches({ a: 1, b: 2 }); +matcher({ a: 1, b: 2, c: 3 }); // true +matcher({ a: 1, c: 3 }); // false +``` + +### 배열 일치 + +```typescript +const arrayMatcher = matches([1, 2, 3]); +arrayMatcher([1, 2, 3, 4]); // true +arrayMatcher([4, 5, 6]); // false +``` + +### 중첩된 구조 일치 + +```typescript +// Matching objects with nested structures +const nestedMatcher = matches({ a: { b: 2 } }); +nestedMatcher({ a: { b: 2, c: 3 } }); // true +nestedMatcher({ a: { c: 3 } }); // false +``` diff --git a/docs/reference/compat/predicate/isMatch.md b/docs/reference/compat/predicate/isMatch.md new file mode 100644 index 00000000..05c96e09 --- /dev/null +++ b/docs/reference/compat/predicate/isMatch.md @@ -0,0 +1,55 @@ +# isMatch + +::: info +This function is fully compatible with lodash. You can find it in our [compatibility library](../../../compatibility.md), `es-toolkit/compat`. +::: + +Checks if the target matches the source by comparing their structures and values. +This function supports deep comparison for objects, arrays, maps, and sets. + +## Signature + +```typescript +function isMatch(target: unknown, source: unknown): boolean; +``` + +## Parameters + +- `target` (`unknown`): The target value to match against. +- `source` (`unknown`): The source value to match with. + +## Returns + +- (`boolean`): Returns `true` if the target matches the source, otherwise `false`. + +## Examples + +### Basic usage + +```typescript +isMatch({ a: 1, b: 2 }, { a: 1 }); // true +``` + +### Matching arrays + +```typescript +isMatch([1, 2, 3], [1, 2, 3]); // true +isMatch([1, 2, 2, 3], [2, 2]); // true +isMatch([1, 2, 3], [2, 2]); // false +``` + +### Matching maps + +```typescript +const targetMap = new Map([['key1', 'value1'], ['key2', 'value2']]); +const sourceMap = new Map([['key1', 'value1']]); +isMatch(targetMap, sourceMap); // true +``` + +### Matching sets + +```javascript +const targetSet = new Set([1, 2, 3]); +const sourceSet = new Set([1, 2]); +isMatch(targetSet, sourceSet); // true +``` diff --git a/docs/reference/compat/predicate/matches.md b/docs/reference/compat/predicate/matches.md new file mode 100644 index 00000000..cced9d87 --- /dev/null +++ b/docs/reference/compat/predicate/matches.md @@ -0,0 +1,51 @@ +# matches + +::: info +This function is fully compatible with lodash. You can find it in our [compatibility library](../../../compatibility.md), `es-toolkit/compat`. +::: + +Creates a function that performs a deep comparison between a given target and the source object. + +This function produces the same results as the [isMatch](./isMatch.md) function, but provides for different ways to call it. + +## Signature + +```typescript +function matches(source: unknown): (target: unknown) => boolean; +``` + +## Parameters + +- `source` (`unknown`): The source object to create the matcher from. + +## Returns + +- (`(target: unknown) => boolean`): Returns a function that takes a target object and returns `true` if the target matches the source, otherwise `false`. + + +## Examples + +### Basic usage + +```typescript +const matcher = matches({ a: 1, b: 2 }); +matcher({ a: 1, b: 2, c: 3 }); // true +matcher({ a: 1, c: 3 }); // false +``` + +### Matching arrays + +```typescript +const arrayMatcher = matches([1, 2, 3]); +arrayMatcher([1, 2, 3, 4]); // true +arrayMatcher([4, 5, 6]); // false +``` + +### Matching nested structures + +```typescript +// Matching objects with nested structures +const nestedMatcher = matches({ a: { b: 2 } }); +nestedMatcher({ a: { b: 2, c: 3 } }); // true +nestedMatcher({ a: { c: 3 } }); // false +``` diff --git a/docs/zh_hans/reference/compat/predicate/isArray.md b/docs/zh_hans/reference/compat/predicate/isArray.md index 61bcef52..1e4cdfea 100644 --- a/docs/zh_hans/reference/compat/predicate/isArray.md +++ b/docs/zh_hans/reference/compat/predicate/isArray.md @@ -1,5 +1,9 @@ # isArray +::: info +此函数与 lodash 完全兼容。您可以在我们的[兼容性库](../../../compatibility.md)中找到它,`es-toolkit/compat`。 +::: + 检查给定的值是否为数组。 该函数测试提供的值是否为数组。 diff --git a/docs/zh_hans/reference/compat/predicate/isMatch.md b/docs/zh_hans/reference/compat/predicate/isMatch.md new file mode 100644 index 00000000..afdd7baf --- /dev/null +++ b/docs/zh_hans/reference/compat/predicate/isMatch.md @@ -0,0 +1,55 @@ +# isMatch + +::: info +此函数与 lodash 完全兼容。您可以在我们的[兼容性库](../../../compatibility.md)中找到它,`es-toolkit/compat`。 +::: + +检查目标是否与源匹配,方法是比较它们的结构和值。 +此函数支持对象、数组、映射和集合的深度比较。 + +## 签名 + +```typescript +function isMatch(target: unknown, source: unknown): boolean; +``` + +## 参数 + +- `target` (`unknown`): 要匹配的目标值。 +- `source` (`unknown`): 用于匹配的源值。 + +## 返回值 + +- (`boolean`): 如果目标与源匹配,则返回 `true`,否则返回 `false`。 + +## 示例 + +### 基本用法 + +```typescript +isMatch({ a: 1, b: 2 }, { a: 1 }); // true +``` + +### 匹配数组 + +```typescript +isMatch([1, 2, 3], [1, 2, 3]); // true +isMatch([1, 2, 2, 3], [2, 2]); // true +isMatch([1, 2, 3], [2, 2]); // false +``` + +### 匹配映射 + +```typescript +const targetMap = new Map([['key1', 'value1'], ['key2', 'value2']]); +const sourceMap = new Map([['key1', 'value1']]); +isMatch(targetMap, sourceMap); // true +``` + +### 匹配集合 + +```typescript +const targetSet = new Set([1, 2, 3]); +const sourceSet = new Set([1, 2]); +isMatch(targetSet, sourceSet); // true +``` diff --git a/docs/zh_hans/reference/compat/predicate/matches.md b/docs/zh_hans/reference/compat/predicate/matches.md new file mode 100644 index 00000000..39e14063 --- /dev/null +++ b/docs/zh_hans/reference/compat/predicate/matches.md @@ -0,0 +1,50 @@ +# matches + +::: info +此函数与 lodash 完全兼容。您可以在我们的[兼容性库](../../../compatibility.md)中找到它,`es-toolkit/compat`。 +::: + +创建一个函数来对给定的目标对象和源对象进行深度比较。 + +这个函数产生与[isMatch](./isMatch.md)函数相同的结果,但提供了不同的调用方式。 + +## 签名 + +```typescript +function matches(source: unknown): (target: unknown) => boolean; +``` + +## 参数 + +- `source` (`unknown`): 用于创建匹配器的源对象。 + +## 返回值 + +- (`(target: unknown) => boolean`): 返回一个函数,该函数接收一个目标对象,如果目标对象与源对象匹配则返回`true`,否则返回`false`。 + +## 示例 + +### 基本用法 + +```typescript +const matcher = matches({ a: 1, b: 2 }); +matcher({ a: 1, b: 2, c: 3 }); // true +matcher({ a: 1, c: 3 }); // false +``` + +### 匹配数组 + +```typescript +const arrayMatcher = matches([1, 2, 3]); +arrayMatcher([1, 2, 3, 4]); // true +arrayMatcher([4, 5, 6]); // false +``` + +### 匹配嵌套结构 + +```typescript +// Matching objects with nested structures +const nestedMatcher = matches({ a: { b: 2 } }); +nestedMatcher({ a: { b: 2, c: 3 } }); // true +nestedMatcher({ a: { c: 3 } }); // false +``` diff --git a/src/compat/_internal/isArrayMatch.spec.ts b/src/compat/_internal/isArrayMatch.spec.ts new file mode 100644 index 00000000..728701d3 --- /dev/null +++ b/src/compat/_internal/isArrayMatch.spec.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest'; +import { isArrayMatch } from './isArrayMatch'; + +describe('isArrayMatch', () => { + it('can match arrays', () => { + expect(isArrayMatch([1, 2, 3], [2, 3])).toBe(true); + expect(isArrayMatch([1, 2, 3, 4, 5], [1, 3, 5])).toBe(true); + expect(isArrayMatch([1, 2, 3, 4, 5], [0, 1])).toBe(false); + }); + + it('can match arrays with duplicated values', () => { + expect(isArrayMatch([2, 2], [2, 2])).toEqual(true); + expect(isArrayMatch([1, 2], [2, 2])).toEqual(false); + }); + + it('returns true if source is empty', () => { + expect(isArrayMatch([1, 2, 3], [])).toBe(true); + expect(isArrayMatch(1, [])).toBe(true); + expect(isArrayMatch(new Map(), [])).toBe(true); + expect(isArrayMatch(new Set(), [])).toBe(true); + }); + + it('can match non-arrays', () => { + expect(isArrayMatch(1, [2, 3])).toBe(false); + expect(isArrayMatch(new Map(), [2, 3])).toBe(false); + expect(isArrayMatch(new Set(), [2, 3])).toBe(false); + }); +}); diff --git a/src/compat/_internal/isArrayMatch.ts b/src/compat/_internal/isArrayMatch.ts new file mode 100644 index 00000000..5074d0c4 --- /dev/null +++ b/src/compat/_internal/isArrayMatch.ts @@ -0,0 +1,28 @@ +import { isMatch } from '../predicate/isMatch.ts'; + +export function isArrayMatch(target: unknown, source: readonly unknown[]) { + if (source.length === 0) { + return true; + } + + if (!Array.isArray(target)) { + return false; + } + + const countedIndex = new Set(); + + for (let i = 0; i < source.length; i++) { + const sourceItem = source[i]; + const index = target.findIndex((targetItem, index) => { + return isMatch(targetItem, sourceItem) && !countedIndex.has(index); + }); + + if (index === -1) { + return false; + } + + countedIndex.add(index); + } + + return true; +} diff --git a/src/compat/_internal/isMapMatch.spec.ts b/src/compat/_internal/isMapMatch.spec.ts new file mode 100644 index 00000000..8c86bb45 --- /dev/null +++ b/src/compat/_internal/isMapMatch.spec.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from 'vitest'; +import { isMapMatch } from './isMapMatch'; + +describe('isMapMatch', () => { + it('can match maps', () => { + expect( + isMapMatch( + new Map([ + ['a', 1], + ['b', 2], + ]), + new Map([ + ['a', 1], + ['b', 2], + ]) + ) + ).toBe(true); + + expect( + isMapMatch( + new Map([ + ['a', 1], + ['b', 2], + ['c', 3], + ]), + new Map([ + ['a', 1], + ['b', 2], + ]) + ) + ).toBe(true); + + expect( + isMapMatch( + new Map([['b', 2]]), + new Map([ + ['a', 1], + ['b', 2], + ]) + ) + ).toBe(false); + + expect( + isMapMatch( + new Map([ + ['a', 2], + ['b', 2], + ]), + new Map([ + ['a', 1], + ['b', 2], + ]) + ) + ).toBe(false); + }); + + it('returns true if source is empty', () => { + const map = new Map(); + + expect( + isMapMatch( + new Map([ + ['a', 2], + ['b', 2], + ]), + map + ) + ).toBe(true); + expect(isMapMatch(1, map)).toBe(true); + expect(isMapMatch('a', map)).toBe(true); + expect(isMapMatch(new Set(), map)).toBe(true); + expect(isMapMatch([1, 2, 3], map)).toBe(true); + expect(isMapMatch({ a: 1, b: 2 }, map)).toBe(true); + }); + + it('returns false if source is not empty and targets that are not maps', () => { + const map = new Map([ + ['a', 1], + ['b', 2], + ]); + + expect(isMapMatch(1, map)).toBe(false); + expect(isMapMatch('a', map)).toBe(false); + expect(isMapMatch(new Set(), map)).toBe(false); + expect(isMapMatch([1, 2, 3], map)).toBe(false); + expect(isMapMatch({ a: 1, b: 2 }, map)).toBe(false); + }); +}); diff --git a/src/compat/_internal/isMapMatch.ts b/src/compat/_internal/isMapMatch.ts new file mode 100644 index 00000000..f8cff3ec --- /dev/null +++ b/src/compat/_internal/isMapMatch.ts @@ -0,0 +1,19 @@ +import { isMatch } from '../predicate/isMatch.ts'; + +export function isMapMatch(target: unknown, source: Map) { + if (source.size === 0) { + return true; + } + + if (!(target instanceof Map)) { + return false; + } + + for (const [key, value] of source.entries()) { + if (!isMatch(target.get(key), value)) { + return false; + } + } + + return true; +} diff --git a/src/compat/_internal/isSetMatch.spec.ts b/src/compat/_internal/isSetMatch.spec.ts new file mode 100644 index 00000000..b094b95d --- /dev/null +++ b/src/compat/_internal/isSetMatch.spec.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; +import { isMapMatch } from './isMapMatch'; +import { isSetMatch } from './isSetMatch'; + +describe('isSetMatch', () => { + it('can match sets', () => { + expect(isSetMatch(new Set([1, 2, 3]), new Set([1, 2, 3]))).toBe(true); + expect(isSetMatch(new Set([1, 2, 3]), new Set([1, 2]))).toBe(true); + expect(isSetMatch(new Set([1, 2]), new Set([1, 2, 3]))).toBe(false); + }); + + it('returns true if source is empty', () => { + const set = new Set(); + + expect(isSetMatch(new Set([1, 2, 3]), set)).toBe(true); + expect(isSetMatch(1, set)).toBe(true); + expect(isSetMatch('a', set)).toBe(true); + expect(isSetMatch(new Set(), set)).toBe(true); + expect(isSetMatch([1, 2, 3], set)).toBe(true); + expect(isSetMatch({ a: 1, b: 2 }, set)).toBe(true); + }); + + it('returns false if source is not empty and target is not a map', () => { + const set = new Set([1, 2, 3]); + + expect(isSetMatch(1, set)).toBe(false); + expect(isSetMatch('a', set)).toBe(false); + expect(isSetMatch(new Set(), set)).toBe(false); + expect(isSetMatch([1, 2, 3], set)).toBe(false); + expect(isSetMatch({ a: 1, b: 2 }, set)).toBe(false); + }); +}); diff --git a/src/compat/_internal/isSetMatch.ts b/src/compat/_internal/isSetMatch.ts new file mode 100644 index 00000000..9e8d9285 --- /dev/null +++ b/src/compat/_internal/isSetMatch.ts @@ -0,0 +1,13 @@ +import { isArrayMatch } from './isArrayMatch.ts'; + +export function isSetMatch(target: unknown, source: Set) { + if (source.size === 0) { + return true; + } + + if (!(target instanceof Set)) { + return false; + } + + return isArrayMatch([...target], [...source]); +} diff --git a/src/compat/_internal/numberProto.ts b/src/compat/_internal/numberProto.ts new file mode 100644 index 00000000..6dd22033 --- /dev/null +++ b/src/compat/_internal/numberProto.ts @@ -0,0 +1 @@ +export const numberProto: any = Number.prototype; \ No newline at end of file diff --git a/src/compat/index.ts b/src/compat/index.ts index 3614d674..2a446d18 100644 --- a/src/compat/index.ts +++ b/src/compat/index.ts @@ -37,6 +37,8 @@ export { set } from './object/set.ts'; export { isPlainObject } from './predicate/isPlainObject.ts'; export { isArray } from './predicate/isArray.ts'; export { isTypedArray } from './predicate/isTypedArray.ts'; +export { isMatch } from './predicate/isMatch.ts'; +export { matches } from './predicate/matches.ts'; export { startsWith } from './string/startsWith.ts'; export { endsWith } from './string/endsWith.ts'; diff --git a/src/compat/predicate/isMatch.spec.ts b/src/compat/predicate/isMatch.spec.ts new file mode 100644 index 00000000..7c2b8a06 --- /dev/null +++ b/src/compat/predicate/isMatch.spec.ts @@ -0,0 +1,320 @@ +import { describe, expect, it } from 'vitest'; +import { noop } from '../../function/noop'; +import { empties } from '../_internal/empties'; +import { stubTrue } from '../_internal/stubTrue'; +import { isMatch } from './isMatch'; + +describe('isMatch', () => { + it(`should perform a deep comparison between \`source\` and \`object\``, () => { + let object: any = { a: 1, b: 2, c: 3 }; + + expect(isMatch(object, { a: 1 })).toBe(true); + expect(isMatch(object, { b: 2 })).toBe(true); + expect(isMatch(object, { a: 1, c: 3 })).toBe(true); + expect(isMatch(object, { c: 3, d: 4 })).toBe(false); + expect(isMatch({ a: { b: { c: 1, d: 2 }, e: 3 }, f: 4 }, { a: { b: { c: 1 } } })).toBe(true); + }); + + it(`should match inherited string keyed \`object\` properties`, () => { + interface Foo { + a: number; + b: number; + } + + interface FooConstructor { + new(): Foo; + } + + const Foo = function Foo(this: Foo) { + this.a = 1; + } as any as FooConstructor; + + Foo.prototype.b = 2; + + const object = { a: new Foo() }; + expect(isMatch(object, { a: { b: 2 } })).toBe(true); + }); + + it(`should not match by inherited \`source\` properties`, () => { + interface Foo { + a: number; + b: number; + } + + interface FooConstructor { + new(): Foo; + } + + const Foo = function Foo(this: Foo) { + this.a = 1; + } as any as FooConstructor; + + Foo.prototype.b = 2; + + const objects = [{ a: 1 }, { a: 1, b: 2 }]; + const source = new Foo(); + const actual = objects.map(object => isMatch(object, source)); + const expected = objects.map(stubTrue); + + expect(actual).toEqual(expected); + }); + + it(`should compare a variety of \`source\` property values`, () => { + const object1 = { a: false, b: true, c: '3', d: 4, e: [5], f: { g: 6 } }; + const object2 = { a: 0, b: 1, c: 3, d: '4', e: ['5'], f: { g: '6' } }; + + expect(isMatch(object1, object1)).toBe(true); + expect(isMatch(object2, object1)).toBe(false); + }); + + it(`should match \`-0\` as \`0\``, () => { + const object1 = { a: -0 }; + const object2 = { a: 0 }; + + expect(isMatch(object2, object1)).toBe(true); + expect(isMatch(object1, object2)).toBe(true); + }); + + it(`should compare functions by reference`, () => { + const object1 = { a: noop }; + const object2 = { a: () => { } }; + const object3 = { a: {} }; + + expect(isMatch(object1, object1)).toBe(true); + expect(isMatch(object2, object1)).toBe(false); + expect(isMatch(object3, object1)).toBe(false); + }); + + it(`should work with a function for \`object\``, () => { + function Foo() { } + Foo.a = { b: 2, c: 3 }; + + expect(isMatch(Foo, { a: { b: 2 } })).toBe(true); + }); + + it(`should work with a function for \`source\``, () => { + function Foo() { } + Foo.a = 1; + Foo.b = function () { }; + Foo.c = 3; + + const objects = [{ a: 1 }, { a: 1, b: Foo.b, c: 3 }]; + const actual = objects.map(object => isMatch(object, Foo)); + + expect(actual).toEqual([false, true]); + }); + + it(`should work with a non-plain \`object\``, () => { + interface Foo { + a: number; + b: number; + c: number; + } + + interface FooConstructor { + new(arg: Partial): Foo; + } + + const Foo = function Foo(this: Foo, object: Partial) { + Object.assign(this, object); + } as any as FooConstructor; + + // eslint-disable-next-line + // @ts-ignore + const object = new Foo({ a: new Foo({ b: 2, c: 3 }) }); + + expect(isMatch(object, { a: { b: 2 } })).toBe(true); + }); + + it(`should partial match arrays`, () => { + const objects = [{ a: ['b'] }, { a: ['c', 'd'] }]; + let actual = objects.filter(x => isMatch(x, { a: ['d'] })); + + expect(actual).toEqual([objects[1]]); + + actual = objects.filter(x => isMatch(x, { a: ['b', 'd'] })); + expect(actual).toEqual([]); + + actual = objects.filter(x => isMatch(x, { a: ['d', 'b'] })); + expect(actual).toEqual([]); + }); + + it(`should partial match arrays with duplicate values`, () => { + const objects = [{ a: [1, 2] }, { a: [2, 2] }]; + const actual = objects.filter(x => isMatch(x, { a: [2, 2] })); + + expect(actual).toEqual([objects[1]]); + }); + + it('should partial match arrays of objects', () => { + const objects = [ + { + a: [ + { b: 1, c: 2 }, + { b: 4, c: 5, d: 6 }, + ], + }, + { + a: [ + { b: 1, c: 2 }, + { b: 4, c: 6, d: 7 }, + ], + }, + ]; + + const actual = objects.filter(x => isMatch(x, { a: [{ b: 1 }, { b: 4, c: 5 }] })); + expect(actual).toEqual([objects[0]]); + }); + + it(`should partial match maps`, () => { + const objects = [{ a: new Map() }, { a: new Map() }]; + objects[0].a.set('a', 1); + objects[1].a.set('a', 1); + objects[1].a.set('b', 2); + + const map = new Map(); + map.set('b', 2); + let actual = objects.filter(x => isMatch(x, { a: map })); + + expect(actual).toEqual([objects[1]]); + + map.delete('b'); + actual = objects.filter(x => isMatch(x, { a: map })); + + expect(actual).toEqual(objects); + + map.set('c', 3); + actual = objects.filter(x => isMatch(x, { a: map })); + + expect(actual).toEqual([]); + }); + + it(`should partial match sets`, () => { + const objects = [{ a: new Set() }, { a: new Set() }]; + objects[0].a.add(1); + objects[1].a.add(1); + objects[1].a.add(2); + + const set = new Set(); + set.add(2); + let actual = objects.filter(x => isMatch(x, { a: set })); + + expect(actual).toEqual([objects[1]]); + + set.delete(2); + actual = objects.filter(x => isMatch(x, { a: set })); + + expect(actual).toEqual(objects); + + set.add(3); + actual = objects.filter(x => isMatch(x, { a: set })); + + expect(actual).toEqual([]); + }); + + it(`should match \`undefined\` values`, () => { + const objects1 = [{ a: 1 }, { a: 1, b: 1 }, { a: 1, b: undefined }]; + const actual1 = objects1.map(x => isMatch(x, { b: undefined })); + const expected1 = [false, false, true]; + + expect(actual1).toEqual(expected1); + + const objects2 = [{ a: 1 }, { a: 1, b: 1 }, { a: 1, b: undefined }]; + const actual2 = objects2.map(x => isMatch(x, { a: 1, b: undefined })); + const expected2 = [false, false, true]; + + expect(actual2).toEqual(expected2); + + const objects3 = [{ a: { b: 2 } }, { a: { b: 2, c: 3 } }, { a: { b: 2, c: undefined } }]; + const actual3 = objects3.map(x => isMatch(x, { a: { c: undefined } })); + const expected3 = [false, false, true]; + + expect(actual3).toEqual(expected3); + }); + + it(`should match \`undefined\` values on primitives`, () => { + const numberProto = Number.prototype; + + // eslint-disable-next-line + // @ts-ignore + numberProto.a = 1; + // eslint-disable-next-line + // @ts-ignore + numberProto.b = undefined; + + try { + expect(isMatch(1, { b: undefined })).toBe(true); + } catch (e: any) { + expect(false, e.message); + } + + try { + expect(isMatch(1, { a: 1, b: undefined })).toBe(true); + } catch (e: any) { + expect(false, e.message); + } + + // eslint-disable-next-line + // @ts-ignore + numberProto.a = { b: 1, c: undefined }; + try { + expect(isMatch(1, { a: { c: undefined } })).toBe(true); + } catch (e: any) { + expect(false, e.message); + } + + // eslint-disable-next-line + // @ts-ignore + delete numberProto.a; + // eslint-disable-next-line + // @ts-ignore + delete numberProto.b; + }); + + it(`should return \`false\` when \`object\` is nullish`, () => { + const values = [, null, undefined]; + const expected = values.map(() => false); + + const actual = values.map((value, index) => { + try { + return index ? isMatch(value, { a: 1 }) : isMatch(undefined, { a: 1 }); + } catch (e: any) { } + }); + + expect(actual).toEqual(expected); + }); + + it(`should return \`true\` when comparing an empty \`source\``, () => { + const object = { a: 1 }; + const expected = empties.map(stubTrue); + + const actual = empties.map(value => { + return isMatch(object, value); + }); + + expect(actual).toEqual(expected); + }); + + it(`should return \`true\` when comparing an empty \`source\` to a nullish \`object\``, () => { + const values = [, null, undefined]; + const expected = values.map(stubTrue); + + const actual = values.map((value, index) => { + try { + return index ? isMatch(value, {}) : isMatch(undefined, {}); + } catch (e: any) { } + }); + + expect(actual).toEqual(expected); + }); + + it(`should return \`true\` when comparing a \`source\` of empty arrays and objects`, () => { + const objects = [ + { a: [1], b: { c: 1 } }, + { a: [2, 3], b: { d: 2 } }, + ]; + const actual = objects.filter(x => isMatch(x, { a: [], b: {} })); + + expect(actual).toEqual(objects); + }); +}); diff --git a/src/compat/predicate/isMatch.ts b/src/compat/predicate/isMatch.ts new file mode 100644 index 00000000..412b7f99 --- /dev/null +++ b/src/compat/predicate/isMatch.ts @@ -0,0 +1,99 @@ +import { isPrimitive } from '../../predicate'; +import { isArrayMatch } from '../_internal/isArrayMatch'; +import { isMapMatch } from '../_internal/isMapMatch'; +import { isSetMatch } from '../_internal/isSetMatch'; + +/** + * Checks if the target matches the source by comparing their structures and values. + * This function supports deep comparison for objects, arrays, maps, and sets. + * + * @param {unknown} target - The target value to match against. + * @param {unknown} source - The source value to match with. + * @returns {boolean} - Returns `true` if the target matches the source, otherwise `false`. + * + * @example + * // Basic usage + * isMatch({ a: 1, b: 2 }, { a: 1 }); // true + * + * @example + * // Matching arrays + * isMatch([1, 2, 3], [1, 2, 3]); // true + * + * @example + * // Matching maps + * const targetMap = new Map([['key1', 'value1'], ['key2', 'value2']]); + * const sourceMap = new Map([['key1', 'value1']]); + * isMatch(targetMap, sourceMap); // true + * + * @example + * // Matching sets + * const targetSet = new Set([1, 2, 3]); + * const sourceSet = new Set([1, 2]); + * isMatch(targetSet, sourceSet); // true + */ +export function isMatch(target: unknown, source: unknown); +export function isMatch(target: any, source: any) { + if (source === target) { + return true; + } + + switch (typeof source) { + case 'object': { + if (source == null) { + return true; + } + + source = source ?? {}; + + const keys = Object.keys(source as any); + + if (target == null) { + if (keys.length === 0) { + return true; + } + + return false; + } + + if (Array.isArray(source)) { + return isArrayMatch(target, source); + } + + if (source instanceof Map) { + return isMapMatch(target, source); + } + + if (source instanceof Set) { + return isSetMatch(target, source); + } + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + + if (!isPrimitive(target) && !(key in target)) { + return false; + } + + if (source[key] === undefined && target[key] !== undefined) { + return false; + } + + if (!isMatch(target[key], source[key])) { + return false; + } + } + + return true; + } + case 'function': { + if (Object.keys(source).length > 0) { + return isMatch(target, { ...source }); + } + + return false; + } + default: { + return !source; + } + } +} diff --git a/src/compat/predicate/matches.spec.ts b/src/compat/predicate/matches.spec.ts new file mode 100644 index 00000000..c6bb7107 --- /dev/null +++ b/src/compat/predicate/matches.spec.ts @@ -0,0 +1,375 @@ +import { describe, expect, it } from 'vitest'; +import { noop } from '../../function/noop'; +import { empties } from '../_internal/empties'; +import { stubTrue } from '../_internal/stubTrue'; +import { matches } from './matches'; + +describe('matches', () => { + it(`should perform a deep comparison between \`source\` and \`object\``, () => { + let object: any = { a: 1, b: 2, c: 3 }; + + const isMatch1 = matches({ a: 1 }); + expect(isMatch1(object)).toBe(true); + + const isMatch2 = matches({ b: 2 }); + expect(isMatch2(object)).toBe(true); + + const isMatch3 = matches({ a: 1, c: 3 }); + expect(isMatch3(object)).toBe(true); + + const isMatch4 = matches({ c: 3, d: 4 }); + expect(isMatch4(object)).toBe(false); + + const isMatch5 = matches({ a: { b: { c: 1 } } }); + expect(isMatch5({ a: { b: { c: 1, d: 2 }, e: 3 }, f: 4 })).toBe(true); + }); + + it(`should match inherited string keyed \`object\` properties`, () => { + interface Foo { + a: number; + b: number; + } + + interface FooConstructor { + new (): Foo; + } + + const Foo = function Foo(this: Foo) { + this.a = 1; + } as any as FooConstructor; + + Foo.prototype.b = 2; + + const object = { a: new Foo() }; + const isMatch = matches({ a: { b: 2 } }); + + expect(isMatch(object)).toBe(true); + }); + + it(`should not match by inherited \`source\` properties`, () => { + interface Foo { + a: number; + b: number; + } + + interface FooConstructor { + new (): Foo; + } + + const Foo = function Foo(this: Foo) { + this.a = 1; + } as any as FooConstructor; + + Foo.prototype.b = 2; + + const objects = [{ a: 1 }, { a: 1, b: 2 }]; + const source = new Foo(); + const actual = objects.map(matches(source)); + const expected = objects.map(stubTrue); + + expect(actual).toEqual(expected); + }); + + it(`should compare a variety of \`source\` property values`, () => { + const object1 = { a: false, b: true, c: '3', d: 4, e: [5], f: { g: 6 } }; + const object2 = { a: 0, b: 1, c: 3, d: '4', e: ['5'], f: { g: '6' } }; + + const isMatch = matches(object1); + + expect(isMatch(object1)).toBe(true); + expect(isMatch(object2)).toBe(false); + }); + + it(`should match \`-0\` as \`0\``, () => { + const object1 = { a: -0 }; + const object2 = { a: 0 }; + + const isMatch1 = matches(object1); + const isMatch2 = matches(object2); + + expect(isMatch1(object2)).toBe(true); + expect(isMatch2(object1)).toBe(true); + }); + + it(`should compare functions by reference`, () => { + const object1 = { a: noop }; + const object2 = { a: () => {} }; + const object3 = { a: {} }; + + const isMatch = matches(object1); + + expect(isMatch(object1)).toBe(true); + expect(isMatch(object2)).toBe(false); + expect(isMatch(object3)).toBe(false); + }); + + it(`should work with a function for \`object\``, () => { + function Foo() {} + Foo.a = { b: 2, c: 3 }; + + const isMatch = matches({ a: { b: 2 } }); + + expect(isMatch(Foo)).toBe(true); + }); + + it(`should work with a function for \`source\``, () => { + function Foo() {} + Foo.a = 1; + Foo.b = function () {}; + Foo.c = 3; + + const objects = [{ a: 1 }, { a: 1, b: Foo.b, c: 3 }]; + const actual = objects.map(matches(Foo)); + + expect(actual).toEqual([false, true]); + }); + + it(`should work with a non-plain \`object\``, () => { + interface Foo { + a: number; + b: number; + c: number; + } + + interface FooConstructor { + new (arg: Partial): Foo; + } + + const Foo = function Foo(this: Foo, object: Partial) { + Object.assign(this, object); + } as any as FooConstructor; + + // eslint-disable-next-line + // @ts-ignore + const object = new Foo({ a: new Foo({ b: 2, c: 3 }) }); + + const isMatch = matches({ a: { b: 2 } }); + + expect(isMatch(object)).toBe(true); + }); + + it(`should partial match arrays`, () => { + const objects = [{ a: ['b'] }, { a: ['c', 'd'] }]; + let actual = objects.filter(matches({ a: ['d'] })); + + expect(actual).toEqual([objects[1]]); + + actual = objects.filter(matches({ a: ['b', 'd'] })); + expect(actual).toEqual([]); + + actual = objects.filter(matches({ a: ['d', 'b'] })); + expect(actual).toEqual([]); + }); + + it(`should partial match arrays with duplicate values`, () => { + const objects = [{ a: [1, 2] }, { a: [2, 2] }]; + const actual = objects.filter(matches({ a: [2, 2] })); + + expect(actual).toEqual([objects[1]]); + }); + + it('should partial match arrays of objects', () => { + const objects = [ + { + a: [ + { b: 1, c: 2 }, + { b: 4, c: 5, d: 6 }, + ], + }, + { + a: [ + { b: 1, c: 2 }, + { b: 4, c: 6, d: 7 }, + ], + }, + ]; + + const actual = objects.filter(matches({ a: [{ b: 1 }, { b: 4, c: 5 }] })); + expect(actual).toEqual([objects[0]]); + }); + + it(`should partial match maps`, () => { + const objects = [{ a: new Map() }, { a: new Map() }]; + objects[0].a.set('a', 1); + objects[1].a.set('a', 1); + objects[1].a.set('b', 2); + + const map = new Map(); + map.set('b', 2); + let actual = objects.filter(matches({ a: map })); + + expect(actual).toEqual([objects[1]]); + + map.delete('b'); + actual = objects.filter(matches({ a: map })); + + expect(actual).toEqual(objects); + + map.set('c', 3); + actual = objects.filter(matches({ a: map })); + + expect(actual).toEqual([]); + }); + + it(`should partial match sets`, () => { + const objects = [{ a: new Set() }, { a: new Set() }]; + objects[0].a.add(1); + objects[1].a.add(1); + objects[1].a.add(2); + + const set = new Set(); + set.add(2); + let actual = objects.filter(matches({ a: set })); + + expect(actual).toEqual([objects[1]]); + + set.delete(2); + actual = objects.filter(matches({ a: set })); + + expect(actual).toEqual(objects); + + set.add(3); + actual = objects.filter(matches({ a: set })); + + expect(actual).toEqual([]); + }); + + it(`should match \`undefined\` values`, () => { + const objects1 = [{ a: 1 }, { a: 1, b: 1 }, { a: 1, b: undefined }]; + const actual1 = objects1.map(matches({ b: undefined })); + const expected1 = [false, false, true]; + + expect(actual1).toEqual(expected1); + + const objects2 = [{ a: 1 }, { a: 1, b: 1 }, { a: 1, b: undefined }]; + const actual2 = objects2.map(matches({ a: 1, b: undefined })); + const expected2 = [false, false, true]; + + expect(actual2).toEqual(expected2); + + const objects3 = [{ a: { b: 2 } }, { a: { b: 2, c: 3 } }, { a: { b: 2, c: undefined } }]; + const actual3 = objects3.map(matches({ a: { c: undefined } })); + const expected3 = [false, false, true]; + + expect(actual3).toEqual(expected3); + }); + + it(`should match \`undefined\` values on primitives`, () => { + const numberProto = Number.prototype; + + // eslint-disable-next-line + // @ts-ignore + numberProto.a = 1; + // eslint-disable-next-line + // @ts-ignore + numberProto.b = undefined; + + try { + const isMatch = matches({ b: undefined }); + expect(isMatch(1)).toBe(true); + } catch (e: any) { + expect(false, e.message); + } + + try { + const isMatch = matches({ a: 1, b: undefined }); + expect(isMatch(1)).toBe(true); + } catch (e: any) { + expect(false, e.message); + } + + // eslint-disable-next-line + // @ts-ignore + numberProto.a = { b: 1, c: undefined }; + try { + const isMatch = matches({ a: { c: undefined } }); + expect(isMatch(1)).toBe(true); + } catch (e: any) { + expect(false, e.message); + } + + // eslint-disable-next-line + // @ts-ignore + delete numberProto.a; + // eslint-disable-next-line + // @ts-ignore + delete numberProto.b; + }); + + it(`should return \`false\` when \`object\` is nullish`, () => { + const values = [, null, undefined]; + const expected = values.map(() => false); + + const isMatch = matches({ a: 1 }); + + const actual = values.map((value, index) => { + try { + return index ? isMatch(value) : isMatch(undefined); + } catch (e: any) {} + }); + + expect(actual).toEqual(expected); + }); + + it(`should return \`true\` when comparing an empty \`source\``, () => { + const object = { a: 1 }; + const expected = empties.map(stubTrue); + + const actual = empties.map(value => { + const isMatch = matches(object); + + return isMatch(object); + }); + + expect(actual).toEqual(expected); + }); + + it(`should return \`true\` when comparing an empty \`source\` to a nullish \`object\``, () => { + const values = [, null, undefined]; + const expected = values.map(stubTrue); + + const isMatch = matches({}); + + const actual = values.map((value, index) => { + try { + return index ? isMatch(value) : isMatch(undefined); + } catch (e: any) {} + }); + + expect(actual).toEqual(expected); + }); + + it(`should return \`true\` when comparing a \`source\` of empty arrays and objects`, () => { + const objects = [ + { a: [1], b: { c: 1 } }, + { a: [2, 3], b: { d: 2 } }, + ]; + const actual = objects.filter(matches({ a: [], b: {} })); + + expect(actual).toEqual(objects); + }); + + it('should not change behavior if `source` is modified', () => { + const sources = [{ a: { b: 2, c: 3 } }, { a: 1, b: 2 }, { a: 1 }]; + + sources.forEach((source: any, index) => { + const object = structuredClone(source); + const isMatch = matches(source); + + expect(isMatch(object)).toBe(true); + + if (index) { + source.a = 2; + source.b = 1; + source.c = 3; + } else { + source.a.b = 1; + source.a.c = 2; + source.a.d = 3; + } + + expect(isMatch(object)).toBe(true); + expect(isMatch(source)).toBe(false); + }); + }); +}); diff --git a/src/compat/predicate/matches.ts b/src/compat/predicate/matches.ts new file mode 100644 index 00000000..de751bf0 --- /dev/null +++ b/src/compat/predicate/matches.ts @@ -0,0 +1,34 @@ +import { cloneDeep } from '../../object/cloneDeep.ts'; +import { isMatch } from './isMatch.ts'; + +/** + * Creates a function that performs a deep comparison between a given target and the source object. + * + * @param {unknown} source - The source object to create the matcher from. + * @returns {(target: unknown) => boolean} - Returns a function that takes a target object and returns `true` if the target matches the source, otherwise `false`. + * + * @example + * // Basic usage + * const matcher = matches({ a: 1, b: 2 }); + * matcher({ a: 1, b: 2, c: 3 }); // true + * matcher({ a: 1, c: 3 }); // false + * + * @example + * // Matching arrays + * const arrayMatcher = matches([1, 2, 3]); + * arrayMatcher([1, 2, 3, 4]); // true + * arrayMatcher([4, 5, 6]); // false + * + * @example + * // Matching objects with nested structures + * const nestedMatcher = matches({ a: { b: 2 } }); + * nestedMatcher({ a: { b: 2, c: 3 } }); // true + * nestedMatcher({ a: { c: 3 } }); // false + */ +export function matches(source: unknown): (target: unknown) => boolean { + source = cloneDeep(source); + + return (target?: unknown): boolean => { + return isMatch(target, source); + }; +} diff --git a/src/object/cloneDeep.spec.ts b/src/object/cloneDeep.spec.ts index 9b7d6059..1ac60321 100644 --- a/src/object/cloneDeep.spec.ts +++ b/src/object/cloneDeep.spec.ts @@ -2,15 +2,6 @@ import { describe, expect, it } from 'vitest'; import { cloneDeep } from './cloneDeep'; describe('cloneDeep', () => { - //------------------------------------------------------------------------------------- - // function - //------------------------------------------------------------------------------------- - it('not support function', () => { - const func = () => {}; - const clonedFunc = cloneDeep(func); - expect(clonedFunc).toBe(undefined); - }); - //------------------------------------------------------------------------------------- // primitive //------------------------------------------------------------------------------------- @@ -167,6 +158,7 @@ describe('cloneDeep', () => { expect(b['#b']).toBe(undefined); expect(b).toEqual({ props: { a: 'es-toolkit' }, + d, c: 2, }); }); diff --git a/src/object/cloneDeep.ts b/src/object/cloneDeep.ts index 03ca6fb2..703aade9 100644 --- a/src/object/cloneDeep.ts +++ b/src/object/cloneDeep.ts @@ -112,7 +112,7 @@ export function cloneDeep(obj: T): Resolved { } if (obj instanceof Error) { - const result = new (obj.constructor as { new (): Error })(); + const result = new (obj.constructor as { new(): Error })(); result.message = obj.message; result.name = obj.name; result.stack = obj.stack; @@ -127,10 +127,6 @@ export function cloneDeep(obj: T): Resolved { return result as Resolved; } - if (typeof obj === 'function') { - return void 0 as Resolved; - } - return obj as Resolved; } @@ -141,12 +137,14 @@ function isPrimitive(value: unknown): value is Primitive { // eslint-disable-next-line function cloneDeepHelper(obj: any, clonedObj: any): void { - for (const key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - const descriptor = Object.getOwnPropertyDescriptor(obj, key); - if ((descriptor?.writable || descriptor?.set) && typeof descriptor?.value !== 'function') { - clonedObj[key] = cloneDeep(obj[key]); - } + const keys = Object.keys(obj); + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const descriptor = Object.getOwnPropertyDescriptor(obj, key); + + if ((descriptor?.writable || descriptor?.set)) { + clonedObj[key] = cloneDeep(obj[key]); } } } @@ -188,82 +186,82 @@ type Equal = X extends Y ? (Y extends X ? true : false) : false; type ResolvedMain = T extends [never] ? never // (special trick for jsonable | null) type : ValueOf extends boolean | number | bigint | string - ? ValueOf - : T extends (...args: any[]) => any - ? never - : T extends object - ? ResolvedObject - : ValueOf; + ? ValueOf + : T extends (...args: any[]) => any + ? never + : T extends object + ? ResolvedObject + : ValueOf; type ResolvedObject = T extends Array - ? IsTuple extends true - ? ResolvedTuple - : Array> - : T extends Set - ? Set> - : T extends Map - ? Map, ResolvedMain> - : T extends WeakSet | WeakMap - ? never - : T extends - | Date - | Uint8Array - | Uint8ClampedArray - | Uint16Array - | Uint32Array - | BigUint64Array - | Int8Array - | Int16Array - | Int32Array - | BigInt64Array - | Float32Array - | Float64Array - | ArrayBuffer - | SharedArrayBuffer - | DataView - | Blob - | File - ? T - : { - [P in keyof T]: ResolvedMain; - }; + ? IsTuple extends true + ? ResolvedTuple + : Array> + : T extends Set + ? Set> + : T extends Map + ? Map, ResolvedMain> + : T extends WeakSet | WeakMap + ? never + : T extends + | Date + | Uint8Array + | Uint8ClampedArray + | Uint16Array + | Uint32Array + | BigUint64Array + | Int8Array + | Int16Array + | Int32Array + | BigInt64Array + | Float32Array + | Float64Array + | ArrayBuffer + | SharedArrayBuffer + | DataView + | Blob + | File + ? T + : { + [P in keyof T]: ResolvedMain; + }; type ResolvedTuple = T extends [] ? [] : T extends [infer F] - ? [ResolvedMain] - : T extends [infer F, ...infer Rest extends readonly any[]] - ? [ResolvedMain, ...ResolvedTuple] - : T extends [(infer F)?] - ? [ResolvedMain?] - : T extends [(infer F)?, ...infer Rest extends readonly any[]] - ? [ResolvedMain?, ...ResolvedTuple] - : []; + ? [ResolvedMain] + : T extends [infer F, ...infer Rest extends readonly any[]] + ? [ResolvedMain, ...ResolvedTuple] + : T extends [(infer F)?] + ? [ResolvedMain?] + : T extends [(infer F)?, ...infer Rest extends readonly any[]] + ? [ResolvedMain?, ...ResolvedTuple] + : []; type IsTuple = [T] extends [never] ? false : T extends readonly any[] - ? number extends T['length'] - ? false - : true - : false; + ? number extends T['length'] + ? false + : true + : false; type ValueOf = IsValueOf extends true - ? boolean - : IsValueOf extends true - ? number - : IsValueOf extends true - ? string - : Instance; + ? boolean + : IsValueOf extends true + ? number + : IsValueOf extends true + ? string + : Instance; type IsValueOf> = Instance extends O ? O extends IValueOf - ? Instance extends Primitive - ? false - : true // not Primitive, but Object - : false // cannot be + ? Instance extends Primitive + ? false + : true // not Primitive, but Object + : false // cannot be : false; interface IValueOf {