Skip to content

Commit

Permalink
feat(clone): enhance type and logic.
Browse files Browse the repository at this point in the history
Current `clone()` function considers only those native classes.

- `Date`
- `Set`
- `Map`
- `RegExp`

However, there are much more native classes in the JavaScript, especially about binary handling.

So, I think that the `clone()` function should consider them.

- `Uint8Array`
- `Uint8ClampedArray`
- `Uint16Array`
- `BigInt64Array`
- `Int8Array`
- `Int16Array`
- `Int32Array`
- `BigInt64Array`
- `Float32Array`
- `Float64Array`
- `ArrayBuffer`
- `SharedArrayBuffer`
- `DataView`
- `Blob`
- `File`

Also, current `clone()` function is returning the same `T` type with its parameter, but it is not correct.

The returned type must be casted, because non-native classes are converted to primitve object type.

To solve this problem, the `clone()` function needs to return `Shallowed<T>` type like below.

```typescript
type Shallowed<T> = Equal<T, ShallowMain<T>> extends true ? T : ShallowMain<T>
type ShallowMain<T> = T extends [never]
  ? never
  : T extends object
  ? T extends
    | Array<any> | Set<any> | Map<any, any> | Date | RegExp | Date
    | Uint8Array
    | Uint8ClampedArray
    | Uint16Array
    | Uint32Array
    | BigUint64Array
    | Int8Array
    | Int16Array
    | Int32Array
    | BigInt64Array
    | Float32Array
    | Float64Array
    | ArrayBuffer
    | SharedArrayBuffer
    | DataView
    | Blob
    | File
  ? T
  : {
    [P in keyof T]: T[P] extends Function ? never : T[P];
  }
  : T;
type Equal<X, Y> = X extends Y ? (Y extends X ? true : false) : false;
```

- related issue: toss#196
- related PR: toss#155
  • Loading branch information
samchon committed Jul 15, 2024
1 parent deb3882 commit 2865f6b
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 30 deletions.
56 changes: 39 additions & 17 deletions src/object/clone.spec.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,52 @@
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);

expect(clonedArr).toEqual(arr);
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);

expect(clonedDate).toEqual(date);
expect(clonedDate).not.toBe(date);
});

it("should clone regular expressions", () => {
it('should clone regular expressions', () => {
const regex = /abc/g;
const clonedRegex = clone(regex);

expect(clonedRegex).toEqual(regex);
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);
Expand All @@ -55,26 +55,48 @@ 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);

expect(clonedSet).toEqual(set);
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<SomeClass> = null!;
let y: SomeInterface = 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;
}
79 changes: 67 additions & 12 deletions src/object/clone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,48 +26,103 @@
* console.log(clonedObj); // { a: 1, b: 'es-toolkit', c: [1, 2, 3] }
* console.log(clonedObj === obj); // false
*/
export function clone<T>(obj: T): T {
export function clone<T>(obj: T): Shallowed<T> {
if (isPrimitive(obj)) {
return obj;
return obj as Shallowed<T>;
}

if (Array.isArray(obj)) {
return obj.slice() as T;
return obj.slice() as Shallowed<T>;
}

if (obj instanceof Date) {
return new Date(obj.getTime()) as T;
return new Date(obj.getTime()) as Shallowed<T>;
}

if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags) as T;
return new RegExp(obj.source, obj.flags) as Shallowed<T>;
}

if (obj instanceof Map) {
const result = new Map();
for (const [key, value] of obj) {
result.set(key, value);
}
return result as T;
return result as Shallowed<T>;
}

if (obj instanceof Set) {
const result = new Set();
for (const value of obj) {
result.add(value);
}
return result as T;
return result as Shallowed<T>;
}

if (typeof obj === "object") {
return Object.assign({}, obj) as T;
// BINARY DATA
if (obj instanceof Uint8Array) return new Uint8Array(obj) as Shallowed<T>;
if (obj instanceof Uint8ClampedArray) return new Uint8ClampedArray(obj) as Shallowed<T>;
if (obj instanceof Uint16Array) return new Uint16Array(obj) as Shallowed<T>;
if (obj instanceof Uint32Array) return new Uint32Array(obj) as Shallowed<T>;
if (obj instanceof BigUint64Array) return new BigUint64Array(obj) as Shallowed<T>;
if (obj instanceof Int8Array) return new Int8Array(obj) as Shallowed<T>;
if (obj instanceof Int16Array) return new Int16Array(obj) as Shallowed<T>;
if (obj instanceof Int32Array) return new Int32Array(obj) as Shallowed<T>;
if (obj instanceof BigInt64Array) return new BigInt64Array(obj) as Shallowed<T>;
if (obj instanceof Float32Array) return new Float32Array(obj) as Shallowed<T>;
if (obj instanceof Float64Array) return new Float64Array(obj) as Shallowed<T>;
if (obj instanceof ArrayBuffer) return obj.slice(0) as Shallowed<T>;
if (obj instanceof SharedArrayBuffer) return obj.slice(0) as Shallowed<T>;
if (obj instanceof DataView) return new DataView(obj.buffer.slice(0)) as Shallowed<T>;
if (obj instanceof Blob) return new Blob([obj], { type: obj.type }) as Shallowed<T>;
if (obj instanceof File) return new File([obj], obj.name, { type: obj.type }) as Shallowed<T>;

if (typeof obj === 'object') {
return Object.assign({}, obj) as Shallowed<T>;
}
return obj;
return obj as Shallowed<T>;
}

export type Shallowed<T> = Equal<T, ShallowMain<T>> extends true ? T : ShallowMain<T>;
type ShallowMain<T> = T extends [never]
? never
: T extends object
? T extends
| Array<any>
| Set<any>
| Map<any, any>
| 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<X, Y> = X extends Y ? (Y extends X ? true : false) : false;
type OmitNever<T extends object> = Omit<T, SpecialFields<T, never>>;
type Primitive = null | undefined | string | number | boolean | symbol | bigint;
type SpecialFields<Instance extends object, Target> = {
[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');
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"lib": ["ESNext"],
"lib": ["DOM", "ESNext"],
"target": "es2016",
"module": "ESNext",
"noEmit": true,
Expand Down

0 comments on commit 2865f6b

Please sign in to comment.