A shallow copy only copies the top - level properties of an object or array. If the original object has nested objects or arrays, the shallow copy will share the references to these nested structures with the original.
// Shallow copy example
const original = { a: 1, b: { c: 2 } };
const shallowCopy = { ...original };
// Modifying the nested object in the shallow copy affects the original
shallowCopy.b.c = 3;
console.log(original.b.c); // Output: 3
On the other hand, a deep copy creates a new object or array with all its nested properties copied recursively, so that any changes made to the copy do not affect the original.
// A simple way to visualize a deep copy conceptually
const original = { a: 1, b: { c: 2 } };
// Assume we have a deepCopy function
const deepCopy = deepCopyFunction(original);
// Modifying the nested object in the deep copy does not affect the original
deepCopy.b.c = 3;
console.log(original.b.c); // Output: 2
One way to perform a deep copy is by implementing a recursive function. This function checks the type of each property and recursively copies nested objects and arrays.
function deepCopy<T>(obj: T): T {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(item => deepCopy(item)) as T;
}
const copy = {} as T;
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepCopy(obj[key]);
}
}
return copy;
}
// Example usage
const original = { a: 1, b: { c: 2 } };
const copied = deepCopy(original);
copied.b.c = 3;
console.log(original.b.c); // Output: 2
A simpler but less robust method is to use JSON.stringify
to convert the object to a string and then JSON.parse
to convert it back to an object.
const original = { a: 1, b: { c: 2 } };
const copied = JSON.parse(JSON.stringify(original));
copied.b.c = 3;
console.log(original.b.c); // Output: 2
However, this method has limitations. It cannot handle functions, Date
objects, RegExp
objects, or objects with circular references.
const original = {
func: () => console.log('Hello'),
date: new Date()
};
const copied = JSON.parse(JSON.stringify(original));
console.log(copied.func); // Output: undefined
console.log(copied.date); // Output: a string representation of the date, not a Date object
Circular references occur when an object references itself directly or indirectly. The JSON.stringify
method will throw an error when encountering circular references. To handle circular references in a deep copy, we can use a WeakMap
to keep track of visited objects.
function deepCopyWithCircular<T>(obj: T): T {
const visited = new WeakMap();
function copy(obj: any) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
if (visited.has(obj)) {
return visited.get(obj);
}
let copyObj;
if (Array.isArray(obj)) {
copyObj = [];
} else {
copyObj = {};
}
visited.set(obj, copyObj);
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
copyObj[key] = copy(obj[key]);
}
}
return copyObj;
}
return copy(obj) as T;
}
// Example with circular reference
const obj = { a: 1 };
obj.b = obj;
const copied = deepCopyWithCircular(obj);
console.log(copied.a); // Output: 1
When performing a deep copy, we need to handle different data types correctly. For example, we should handle Date
objects, RegExp
objects, and custom classes properly.
function deepCopyWithTypes<T>(obj: T): T {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
if (obj instanceof Date) {
return new Date(obj.getTime()) as T;
}
if (obj instanceof RegExp) {
return new RegExp(obj) as T;
}
if (Array.isArray(obj)) {
return obj.map(item => deepCopyWithTypes(item)) as T;
}
const copy = {} as T;
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepCopyWithTypes(obj[key]);
}
}
return copy;
}
// Example with Date object
const original = { date: new Date() };
const copied = deepCopyWithTypes(original);
console.log(copied.date instanceof Date); // Output: true
The recursive approach can be slow for large and deeply nested objects. If performance is a concern, consider using a more optimized library or caching intermediate results.
When implementing a deep copy function, add proper error handling. For example, if an object has a property that cannot be serialized or deserialized, the function should handle it gracefully instead of crashing.
Thoroughly test your deep copy function with different types of objects, including nested objects, arrays, circular references, and different data types. This ensures that the function works correctly in all scenarios.
Deep copying in TypeScript is an important concept when working with complex data structures. Understanding the difference between shallow and deep copy, and knowing how to implement a deep copy using various methods is crucial. The manual recursive approach gives you full control but requires careful handling of different data types and circular references. The JSON.stringify
and JSON.parse
method is simple but has limitations. By following the common and best practices, you can create a robust and efficient deep copy function for your TypeScript projects.