How to Deep Clone an Object in TypeScript (4 Ways)

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 lib setting with dom, or @types/node, so the compiler knows structuredClone.
  • Node.js 17+ or a current browser.
  • This sample order, with a nested object, an array, a Set and a Date:
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
VS Code terminal showing a TypeScript spread copy where changing nested city and qty also changes the original object
The id stayed ORD-2041, but the original’s city and quantity changed.

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
VS Code terminal showing TypeScript structuredClone copy changes leaving the original city, quantity and tags unchanged
The original keeps Austin, quantity 2 and two tags.

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
VS Code terminal showing a TypeScript JSON clone turning a Date into a string and a Set into an empty object
The date comes back as a string, the Set is empty, and getFullYear fails at run time.

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
VS Code terminal showing a TypeScript structuredClone of a class instance losing its methods while a clone method works
The structured clone lost its total method; clone() works.

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:

MethodNested objectsDate / SetClass methodsCircular refs
{ ...obj }SharedSharedLostShared
structuredClone()CopiedKeptLostKept
JSON.parse(JSON.stringify())CopiedBrokenLostThrows
Typed deepClone()CopiedKeptKeptNot 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:

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.

Can structuredClone() copy fu