diff --git a/src/object/clone.spec.ts b/src/object/clone.spec.ts index 4af6a7fec..34f882510 100644 --- a/src/object/clone.spec.ts +++ b/src/object/clone.spec.ts @@ -1,19 +1,19 @@ -import { describe, expect, it } from "vitest"; -import { clone } from "./clone"; +import { describe, expect, it } from 'vitest'; +import { clone, Shallowed } from './clone'; -describe("clone", () => { - it("should return primitive values as is", () => { - const symbol = Symbol("symbol"); +describe('clone', () => { + it('should return primitive values as is', () => { + const symbol = Symbol('symbol'); expect(clone(42)).toBe(42); - expect(clone("es-toolkit")).toBe("es-toolkit"); + expect(clone('es-toolkit')).toBe('es-toolkit'); expect(clone(symbol)).toBe(symbol); expect(clone(true)).toBe(true); expect(clone(null)).toBe(null); expect(clone(undefined)).toBe(undefined); }); - it("should clone arrays", () => { + it('should clone arrays', () => { const arr = [1, 2, 3]; const clonedArr = clone(arr); @@ -21,15 +21,15 @@ describe("clone", () => { expect(clonedArr).not.toBe(arr); }); - it("should clone objects", () => { - const obj = { a: 1, b: "es-toolkit", c: [1, 2, 3] }; + it('should clone objects', () => { + const obj = { a: 1, b: 'es-toolkit', c: [1, 2, 3] }; const clonedObj = clone(obj); expect(clonedObj).toEqual(obj); expect(clonedObj).not.toBe(obj); }); - it("should clone dates", () => { + it('should clone dates', () => { const date = new Date(); const clonedDate = clone(date); @@ -37,7 +37,7 @@ describe("clone", () => { expect(clonedDate).not.toBe(date); }); - it("should clone regular expressions", () => { + it('should clone regular expressions', () => { const regex = /abc/g; const clonedRegex = clone(regex); @@ -45,8 +45,8 @@ describe("clone", () => { expect(clonedRegex).not.toBe(regex); }); - it("should shallow clone nested objects", () => { - const nestedObj = { a: [1, 2, 3], b: { c: "es-toolkit" }, d: new Date() }; + it('should shallow clone nested objects', () => { + const nestedObj = { a: [1, 2, 3], b: { c: 'es-toolkit' }, d: new Date() }; const clonedNestedObj = clone(nestedObj); expect(clonedNestedObj).toEqual(nestedObj); @@ -55,14 +55,14 @@ describe("clone", () => { expect(clonedNestedObj.a[2]).toEqual(nestedObj.a[2]); }); - it("should return functions as is", () => { + it('should return functions as is', () => { const func = () => {}; const clonedFunc = clone(func); expect(clonedFunc).toBe(func); }); - it("should clone sets", () => { + it('should clone sets', () => { const set = new Set([1, 2, 3]); const clonedSet = clone(set); @@ -70,11 +70,33 @@ describe("clone", () => { expect(clonedSet).not.toBe(set); }); - it("should clone maps", () => { - const map = new Map([[1, "a"], [2, "b"], [3, "c"]]); + it('should clone maps', () => { + const map = new Map([ + [1, 'a'], + [2, 'b'], + [3, 'c'], + ]); const clonedMap = clone(map); expect(clonedMap).toEqual(map); expect(clonedMap).not.toBe(map); }); + + // check whether two types are equal only in the compiler level + let x: Shallowed = null!; + let y: Shallowed = null!; + x = y; + y = x; }); + +declare class SomeClass { + public id: string; + public name: string; + public age: number; + public getName(): string; +} +interface SomeInterface { + id: string; + name: string; + age: number; +} diff --git a/src/object/clone.ts b/src/object/clone.ts index 7cccdf6a7..a49d1bbde 100644 --- a/src/object/clone.ts +++ b/src/object/clone.ts @@ -26,48 +26,109 @@ * console.log(clonedObj); // { a: 1, b: 'es-toolkit', c: [1, 2, 3] } * console.log(clonedObj === obj); // false */ -export function clone(obj: T): T { +export function clone(obj: T): Shallowed { if (isPrimitive(obj)) { - return obj; + return obj as Shallowed; } if (Array.isArray(obj)) { - return obj.slice() as T; + return obj.slice() as Shallowed; } - if (obj instanceof Date) { - return new Date(obj.getTime()) as T; - } + if (typeof obj === 'object') { + // NATIVE CLASSES + if (obj instanceof Date) { + return new Date(obj.getTime()) as Shallowed; + } - if (obj instanceof RegExp) { - return new RegExp(obj.source, obj.flags) as T; - } + if (obj instanceof RegExp) { + return new RegExp(obj.source, obj.flags) as Shallowed; + } - if (obj instanceof Map) { - const result = new Map(); - for (const [key, value] of obj) { - result.set(key, value); + if (obj instanceof Map) { + const result = new Map(); + for (const [key, value] of obj) { + result.set(key, value); + } + return result as Shallowed; } - return result as T; - } - if (obj instanceof Set) { - const result = new Set(); - for (const value of obj) { - result.add(value); + if (obj instanceof Set) { + const result = new Set(); + for (const value of obj) { + result.add(value); + } + return result as Shallowed; + } + + // BINARY DATA + if (obj instanceof Uint8Array) return new Uint8Array(obj) as Shallowed; + if (obj instanceof Uint8ClampedArray) return new Uint8ClampedArray(obj) as Shallowed; + if (obj instanceof Uint16Array) return new Uint16Array(obj) as Shallowed; + if (obj instanceof Uint32Array) return new Uint32Array(obj) as Shallowed; + if (obj instanceof BigUint64Array) return new BigUint64Array(obj) as Shallowed; + if (obj instanceof Int8Array) return new Int8Array(obj) as Shallowed; + if (obj instanceof Int16Array) return new Int16Array(obj) as Shallowed; + if (obj instanceof Int32Array) return new Int32Array(obj) as Shallowed; + if (obj instanceof BigInt64Array) return new BigInt64Array(obj) as Shallowed; + if (obj instanceof Float32Array) return new Float32Array(obj) as Shallowed; + if (obj instanceof Float64Array) return new Float64Array(obj) as Shallowed; + if (obj instanceof ArrayBuffer) return obj.slice(0) as Shallowed; + if (obj instanceof SharedArrayBuffer) return obj.slice(0) as Shallowed; + if (obj instanceof DataView) return new DataView(obj.buffer.slice(0)) as Shallowed; + if (typeof File !== 'undefined') { + // For legacy NodeJS support + if (obj instanceof File) return new File([obj], obj.name, { type: obj.type }) as Shallowed; + if (obj instanceof Blob) return new Blob([obj], { type: obj.type }) as Shallowed; } - return result as T; - } - if (typeof obj === "object") { - return Object.assign({}, obj) as T; + return Object.fromEntries( + Object.entries(obj as any).filter(([_key, value]) => value !== undefined && typeof value !== 'function') + ) as Shallowed; } - return obj; + return obj as Shallowed; } +export type Shallowed = Equal> extends true ? T : ShallowMain; +type ShallowMain = T extends [never] + ? never + : T extends object + ? T extends + | Array + | Set + | Map + | Date + | RegExp + | Date + | Uint8Array + | Uint8ClampedArray + | Uint16Array + | Uint32Array + | BigUint64Array + | Int8Array + | Int16Array + | Int32Array + | BigInt64Array + | Float32Array + | Float64Array + | ArrayBuffer + | SharedArrayBuffer + | DataView + | Blob + | File + ? T + : OmitNever<{ + [P in keyof T]: T[P] extends Function ? never : T[P]; + }> + : T; + +type Equal = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? true : false; +type OmitNever = Omit>; type Primitive = null | undefined | string | number | boolean | symbol | bigint; +type SpecialFields = { + [P in keyof Instance]: Instance[P] extends Target ? P : never; +}[keyof Instance & string]; function isPrimitive(value: unknown): value is Primitive { - return value == null || - (typeof value !== "object" && typeof value !== "function"); + return value == null || (typeof value !== 'object' && typeof value !== 'function'); } diff --git a/tsconfig.json b/tsconfig.json index 5cfa45c8d..847845413 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "lib": ["ESNext"], + "lib": ["DOM", "ESNext"], "target": "es2016", "module": "ESNext", "noEmit": true,