You try to deep clone a TypeScript order object with the spread operator, change the city on the copy, and the original moves to Denver too.
Quick answer: call structuredClone(). It copies every nested level, keeps Dates and Sets, and returns the input’s type:
const copy = structuredClone(order);
In this tutorial, we’ll deep clone an object in TypeScript four ways and fix the errors teams hit.
I ran every step with TypeScript 7.0.2 and Node.js v22.22.2 in strict mode, on one sample order.
What You Need Before You Deep Clone an Object
- A
libsetting withdom, or@types/node, so the compiler knowsstructuredClone. - Node.js 17+ or a current browser.
- This sample order, with a nested object, an array, a
Setand aDate:
const order = {
id: "ORD-2041",
customer: { name: "Megan Bowen", city: "Austin" },
items: [{ sku: "LAMP-01", qty: 2, price: 39.99 }],
tags: new Set(["gift", "rush"]),
createdAt: new Date("2026-10-28T09:30:00Z"),
};
New to spread syntax? Start with our TypeScript spread operator guide.
Step 1: See Why a Spread Copy Is Shallow
We copy the order with { ...order } and change the id, city and quantity on the copy:
// order: the sample order from above
const copy = { ...order };
copy.id = "ORD-2042";
copy.customer.city = "Denver";
copy.items[0].qty = 5;
console.log("order.id: ", order.id);
console.log("order city:", order.customer.city);
console.log("order qty: ", order.items[0].qty);
Output:
order.id: ORD-2041
order city: Denver
order qty: 5

Spread copies only the top level, so customer and items stay shared. That’s a shallow copy, and merging objects in TypeScript with spread or Object.assign() works the same way.
Step 2: Deep Clone With structuredClone()
Same order, now cloned with structuredClone(). We change the copy at three levels:
// order: the sample order from above
const copy = structuredClone(order); // same type as order
copy.customer.city = "Denver";
copy.items[0].qty = 5;
copy.tags.add("vip");
console.log("order city:", order.customer.city);
console.log("order qty: ", order.items[0].qty);
console.log("order tags:", order.tags);
console.log("copy date is a Date?", copy.createdAt instanceof Date);
Output:
order city: Austin
order qty: 2
order tags: Set(2) { 'gift', 'rush' }
copy date is a Date? true

This is our default. No cast is needed, and it’s ideal when you update an object in an array without touching the source.
Step 3: Clone With JSON.parse(JSON.stringify())
Older code often uses a JSON round trip. Here’s the same order through it:
// order: the sample order from above
const copy: typeof order = JSON.parse(JSON.stringify(order));
console.log("createdAt:", typeof copy.createdAt, copy.createdAt);
console.log("tags:", copy.tags);
try {
copy.createdAt.getFullYear();
} catch (e) {
console.log("Error:", (e as Error).message);
}
Output:
createdAt: string 2026-10-28T09:30:00.000Z
tags: {}
Error: copy.createdAt.getFullYear is not a function

Important: TypeScript doesn’t warn you, because JSON.parse() returns any. Keep it for plain data, as in converting objects to JSON.
Step 4: Write a Typed deepClone() Function
For full control, a small generic function handles Dates, Sets, arrays and plain objects, and keeps each prototype:
function deepClone<T>(value: T): T {
if (value === null || typeof value !== "object") return value;
if (value instanceof Date) return new Date(value) as T;
if (value instanceof Set) return new Set([...value].map(deepClone)) as T;
if (Array.isArray(value)) return value.map(deepClone) as T;
const out = Object.create(Object.getPrototypeOf(value));
for (const key of Object.keys(value)) {
out[key] = deepClone((value as Record<string, unknown>)[key]);
}
return out;
}
// order: the sample order from above
const copy = deepClone(order);
copy.items[0].qty = 5;
console.log("order qty:", order.items[0].qty);
console.log("copy date:", copy.createdAt instanceof Date);
Output:
order qty: 2
copy date: true
The original quantity stays 2 and the date is still a Date. The <T> keeps the input type; see generic object types.
Note: it skips Maps and circular references; Lodash’s cloneDeep covers both.
Step 5: Deep Clone Class Instances
Here structuredClone() falls short, as this Cart class shows:
class Cart {
constructor(public items: number[]) {}
total(): number {
return this.items.reduce((sum, price) => sum + price, 0);
}
clone(): Cart {
return new Cart([...this.items]);
}
}
const cart = new Cart([19.99, 5.5]);
const sc = structuredClone(cart);
console.log("instanceof Cart:", sc instanceof Cart);
try {
sc.total();
} catch (e) {
console.log("Error:", (e as Error).message);
}
const own = cart.clone();
own.items.push(10);
console.log("clone total: $" + own.total().toFixed(2));
console.log("cart total: $" + cart.total().toFixed(2));
Output:
instanceof Cart: false
Error: sc.total is not a function
clone total: $35.49
cart total: $25.49

TypeScript still types the plain result as Cart. Give classes their own clone() method; our TypeScript class guide covers methods.
Deep Clone Methods Cheat Sheet
Here’s what the runs above showed:
| Method | Nested objects | Date / Set | Class methods | Circular refs |
|---|---|---|---|---|
{ ...obj } | Shared | Shared | Lost | Shared |
structuredClone() | Copied | Kept | Lost | Kept |
JSON.parse(JSON.stringify()) | Copied | Broken | Lost | Throws |
Typed deepClone() | Copied | Kept | Kept | Not handled |
Common Mistakes to Avoid When Cloning Objects
We reproduced the two errors teams hit most:
try {
structuredClone({ greet: () => "Hi" });
} catch (e) {
console.log((e as Error).name + ":", (e as Error).message);
}
const node: { self?: object } = {};
node.self = node;
try {
JSON.stringify(node);
} catch (e) {
const msg = (e as Error).message.split("\n")[0];
console.log((e as Error).name + ":", msg);
}
const c = structuredClone(node);
console.log("structuredClone keeps the loop:", c.self === c);
Output:
DataCloneError: () => "Hi" could not be cloned.
TypeError: Converting circular structure to JSON
structuredClone keeps the loop: true
Functions can’t be cloned, and JSON can’t handle loops, while structuredClone() can. Catch both with try/catch in TypeScript.
Does structuredClone() Work in Node.js and the Browser?
Yes. It’s a global in Node.js 17+ and all current browsers, so there’s nothing to import.
Next Steps After Deep Cloning
If you clone to avoid mutation, readonly arrays in TypeScript let the compiler block changes instead.
We’d keep these close by:
- Reverse an array in TypeScript without mutating it
- Initialize a Map in TypeScript
- Check for empty objects in TypeScript
- Shallow copy vs deep copy in Python
Frequently asked questions
How do I deep clone an object in TypeScript?
Use structuredClone(obj). It copies every level and keeps the input type.
Is the spread operator a deep copy?
No. { ...obj } copies only the top level; nested objects stay shared.
Why does my Date become a string after cloning?
JSON stores dates as strings and JSON.parse() doesn’t turn them back.
Does structuredClone() keep class methods?
No. It returns a plain object. Add a clone() method to the class.