Deep clone without JSON.parse
typescriptA production-ready deep clone utility that leverages the native structuredClone API when available, with a comprehensive manual fallback that correctly handles special object types and circular references.
Code
type Cloneable =
| null
| undefined
| boolean
| number
| string
| Date
| RegExp
| Map<Cloneable, Cloneable>
| Set<Cloneable>
| Cloneable[]
| { [key: string]: Cloneable };
function deepCloneManual<T extends Cloneable>(
source: T,
visited: WeakMap<object, unknown> = new WeakMap()
): T {
if (source === null || typeof source !== 'object') {
return source;
}
if (visited.has(source as object)) {
return visited.get(source as object) as T;
}
if (source instanceof Date) {
return new Date(source.getTime()) as T;
}
if (source instanceof RegExp) {
return new RegExp(source.source, source.flags) as T;
}
if (source instanceof Map) {
const clonedMap = new Map();
visited.set(source as object, clonedMap);
source.forEach((value, key) => {
clonedMap.set(
deepCloneManual(key as Cloneable, visited),
deepCloneManual(value as Cloneable, visited)
);
});
return clonedMap as T;
}
if (source instanceof Set) {
const clonedSet = new Set();
visited.set(source as object, clonedSet);
source.forEach((value) => {
clonedSet.add(deepCloneManual(value as Cloneable, visited));
});
return clonedSet as T;
}
if (Array.isArray(source)) {
const clonedArray: Cloneable[] = [];
visited.set(source as object, clonedArray);
for (const item of source) {
clonedArray.push(deepCloneManual(item, visited));
}
return clonedArray as T;
}
const clonedObject: Record<string, Cloneable> = {};
visited.set(source as object, clonedObject);
for (const key of Object.keys(source)) {
clonedObject[key] = deepCloneManual(
(source as Record<string, Cloneable>)[key],
visited
);
}
return clonedObject as T;
}
function deepClone<T extends Cloneable>(source: T): T {
if (typeof structuredClone === 'function') {
try {
return structuredClone(source);
} catch {
return deepCloneManual(source);
}
}
return deepCloneManual(source);
}
const original = {
name: 'Test',
date: new Date('2024-01-15'),
nested: { level: 1 },
items: new Set([1, 2, 3]),
metadata: new Map([['key', 'value']]),
pattern: /test/gi,
};
const circularObj: Record<string, unknown> = { a: 1 };
circularObj.self = circularObj;
const cloned = deepClone(original);
const clonedCircular = deepClone(circularObj as Cloneable);
console.log('Original:', original);
console.log('Cloned:', cloned);
console.log('Are different objects:', original !== cloned);
console.log('Date cloned correctly:', cloned.date instanceof Date);
console.log('Set cloned correctly:', cloned.items instanceof Set);
console.log('Map cloned correctly:', cloned.metadata instanceof Map);
console.log('Circular handled:', (clonedCircular as Record<string, unknown>).self === clonedCircular);How It Works
This deep clone implementation follows a progressive enhancement pattern, preferring the native structuredClone API when available while providing a robust manual fallback. The structuredClone function, introduced in modern browsers and Node.js 17+, is the most reliable built-in method for deep cloning, handling circular references and special object types natively. However, since it's not universally available, the fallback ensures compatibility across all JavaScript environments.
The manual cloning function uses a WeakMap to track visited objects, which is essential for handling circular references. When an object is first encountered, it's added to the WeakMap before its properties are cloned. If the same object is encountered again during the cloning process, the previously created clone is returned instead of creating an infinite loop. WeakMap is preferred over Map here because it allows garbage collection of the tracked objects once cloning is complete.
Special object types require individual handling because they store data differently than plain objects. Dates are cloned by extracting the timestamp with getTime() and creating a new Date instance. RegExp objects need both their source pattern and flags preserved. Maps and Sets are handled by creating new instances and iterating through their entries, recursively cloning each value. This approach ensures that nested special types within Maps or Sets are also properly cloned.
The TypeScript type system helps ensure type safety through the Cloneable union type, which defines all supported types. The generic constraint T extends Cloneable preserves the specific type of the input through the cloning process, so deepClone(myDate) returns a Date type rather than a generic Cloneable. Note that this implementation doesn't handle functions, WeakMaps, WeakSets, or class instances with custom prototypes—these are intentionally excluded as they typically shouldn't be cloned in most use cases.
Use this utility when you need complete independence between original and cloned data structures, such as in state management, undo/redo systems, or when passing data between modules that shouldn't share references. Avoid it for performance-critical code with large objects where shallow copies or immutable data patterns might be more appropriate.