Skip to content
Open
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
31 changes: 5 additions & 26 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -625,39 +625,18 @@ export function groupBy<T, K extends string | number | symbol>(
);
}

function cloneArray<T>(arr: T[]): T[] {
return arr.map((e) => cloneDeep(e));
}

function cloneObject<T extends Record<string, unknown>>(obj: T): T {
const clone: Record<string, unknown> = {};
for (const [k, v] of Object.entries(obj)) {
clone[k] = cloneDeep(v);
}
return clone as T;
}

/**
* replacement for lodash cloneDeep that preserves type.
*/
// TODO: replace with builtin once Node 18 becomes the min version.
export function cloneDeep<T>(obj: T): T {
if (typeof obj !== "object" || !obj) {
if (obj === undefined) {
return obj;
}
if (obj instanceof RegExp) {
return RegExp(obj, obj.flags) as typeof obj;
}
if (obj instanceof Date) {
return new Date(obj) as typeof obj;
}
if (Array.isArray(obj)) {
return cloneArray(obj) as typeof obj;
}
if (obj instanceof Map) {
return new Map(obj.entries()) as typeof obj;
try {
return structuredClone(obj);
} catch (e) {
return obj;
}
return cloneObject(obj as Record<string, unknown>) as typeof obj;
}
Comment on lines 631 to 640

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The new implementation introduces two significant issues:

  1. Correctness Regression (High Severity): If structuredClone throws an error (e.g., when the object contains a function, symbol, or other non-serializable property), the catch block silently returns the original object reference obj. This is a regression from the original cloneDeep which recursively cloned the object structure and copied functions by reference, ensuring the outer object reference was broken. Returning the original reference means any subsequent mutations to the "cloned" object will unexpectedly mutate the original object, leading to silent and hard-to-debug bugs.
  2. Performance Regression (Medium Severity): The fast-path check for primitives, null, and functions has been removed. Only checking obj === undefined means all other primitives (like strings, numbers, booleans, null) and functions will go through the try/catch block and call structuredClone(), which is much slower than a simple type check.

We can resolve both issues by restoring the fast-path check and falling back to _.cloneDeep (which is already imported in this file from lodash) when structuredClone fails.

Suggested change
export function cloneDeep<T>(obj: T): T {
if (typeof obj !== "object" || !obj) {
if (obj === undefined) {
return obj;
}
if (obj instanceof RegExp) {
return RegExp(obj, obj.flags) as typeof obj;
}
if (obj instanceof Date) {
return new Date(obj) as typeof obj;
}
if (Array.isArray(obj)) {
return cloneArray(obj) as typeof obj;
}
if (obj instanceof Map) {
return new Map(obj.entries()) as typeof obj;
try {
return structuredClone(obj);
} catch (e) {
return obj;
}
return cloneObject(obj as Record<string, unknown>) as typeof obj;
}
export function cloneDeep<T>(obj: T): T {
if (typeof obj !== "object" || !obj) {
return obj;
}
try {
return structuredClone(obj);
} catch (e) {
return _.cloneDeep(obj);
}
}
References
  1. Reduce nesting as much as possible: Code should avoid unnecessarily deep nesting or long periods of nesting. Use early returns, continue, and break statements in functions and loops to handle edge cases early and keep main logic flat. (link)


/**
Expand Down