From 2e68eb6219ef16f1d740bca767c64fe4b68328e9 Mon Sep 17 00:00:00 2001 From: Avior Date: Wed, 30 Jul 2025 11:13:36 +0200 Subject: [PATCH] fix: objectClone not correctly cloning a date Signed-off-by: Avior --- packages/object-util/__tests__/index.test.ts | 42 ++++++++++++++++++++ packages/object-util/src/ObjectUtil.ts | 27 +++++++++++-- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/packages/object-util/__tests__/index.test.ts b/packages/object-util/__tests__/index.test.ts index beb05fa..1f91938 100644 --- a/packages/object-util/__tests__/index.test.ts +++ b/packages/object-util/__tests__/index.test.ts @@ -1,5 +1,6 @@ /// +import { objectPick } from '../dist/ObjectUtil' import { isObject, objectClean, objectClone, objectEqual, objectFind, objectGet, objectKeys, objectLoop, objectMap, objectOmit, objectRemap, objectSet, objectSize, objectSort, objectValues } from '../src/ObjectUtil' describe('Throw if parameter is not an object', () => { @@ -220,6 +221,25 @@ describe('Object Clone Tests', () => { expect(clone).not.toEqual(obj) }) + it('it should clone dates', () => { + const date = new Date('2025-01-02') + const obj = { + pouet: date, + } + + // check base clone + const clone = objectClone(obj) + expect(clone).toEqual(obj) + + // check that it is deep (date changes doesn't affect the clone) + date.setFullYear(2020) + expect(clone).not.toEqual(obj) + + // check a value change doesn't affect the clone + clone.pouet = new Date() + expect(clone).not.toEqual(obj) + }) + it('should deeply clone the object when option is set', () => { const obj = { pouet: {is: 'first'}, @@ -416,6 +436,28 @@ describe('Object Omit Tests', () => { }) }) +describe('Object Pick Tests', () => { + it('should pick certain elements', () => { + const obj = {a: 'a', b: 'c', c: 'b'} + expect(objectPick(obj, 'b')).toEqual({b: 'c'}) + }) + + it('should not care when key to pick is not present', () => { + const obj = {a: 'a', b: 'c', c: 'b'} + expect(objectPick(obj, 'b', 'd')).toEqual({b: 'c'}) + }) + + it('should work with Object.freeze', () => { + const obj = {a: 'a', b: 'c', c: 'b'} + expect(objectPick(Object.freeze(obj), 'b', 'd')).toEqual({b: 'c'}) + }) + + it('should work with an array', () => { + const obj = [1, 2, 3, 4] + expect(objectPick(obj, 1, 3)).toEqual([undefined,2,undefined,4]) + }) +}) + describe('Is Object Tests', () => { it('null is not an "object"', () => { expect(isObject(null)).toBe(false) diff --git a/packages/object-util/src/ObjectUtil.ts b/packages/object-util/src/ObjectUtil.ts index bacb245..c828f96 100644 --- a/packages/object-util/src/ObjectUtil.ts +++ b/packages/object-util/src/ObjectUtil.ts @@ -1,6 +1,11 @@ export type BasicObjectKeys = string | number | symbol export type BasicObject = { [P in K]?: V } +/** + * Pick a from a text union only elements in Keys + */ +type TextPick = T extends Keys ? T : never + /** * Remap an object to an array through a function * @@ -187,10 +192,24 @@ export function objectClone(obj: T, options?: { deep?: bo } const clone: Partial = {} objectLoop(obj, (value, key) => { - if (typeof value === 'object' && value != null && (typeof options?.deep === 'undefined' || options.deep)) { + if ( + typeof value === 'object' // make sure child is an object + && value != null // object is not null + && ( + typeof options?.deep === 'undefined' + || options.deep + ) // deep is set to true or undefined + && !(value instanceof Date) // object is not a date + ) { clone[key as Extract] = objectClone(value) return } + // special case for Date + if (value instanceof Date) { + // @ts-expect-error value type is a Date + clone[key as Extract] = new Date(value.getTime()) + return + } clone[key as Extract] = value }) return clone as T @@ -302,7 +321,7 @@ export function objectClean(obj: BasicObject, options?: { cleanUndefined?: boole * @param keys the keys to emit * @returns the cloned object */ -export function objectOmit(obj: T, ...keys: Array): T { +export function objectOmit(obj: T, ...keys: Array): Omit { const cloned = objectClone(obj, { deep: false }) for (const key of keys) { if (key in cloned) { @@ -392,9 +411,9 @@ export function objectGet(obj: any, path: Array(obj: Record, ...keys: Array): Pick, K> { +export function objectPick(obj: T, ...keys: Array): Pick> { mustBeObject(obj) - return objectFilter(obj, (_, k) => keys.includes(k)) as Pick, K> + return objectFilter(obj, (_, k) => keys.includes(k)) as Pick } /**