structuredClone for a true deep copy, no JSON hacks
Published September 25, 2026
type Session = { id: string; startedAt: Date; roles: Set<string>; meta: Map<string, number> };
export const cloneSession = (session: Session): Session => structuredClone(session);
const original: Session = {
id: "abc123",
startedAt: new Date(),
roles: new Set(["admin", "editor"]),
meta: new Map([["retries", 0]]),
};
const copy = cloneSession(original);
copy.roles.add("viewer");
console.log(original.roles.size, copy.roles.size); // 2 3 — independent copiesWhat
structuredClone(value) is a global function, available in Node with no import, that produces a real deep copy of a value — including types JSON.stringify can't represent at all.
Why it matters
JSON.parse(JSON.stringify(value)) has been the go-to deep-copy hack for years, and it mostly works — right up until the value contains something JSON can't express. A Date round-trips as a string, not a Date. A Map or Set round-trips as {} — silently, with no error, just quietly wrong data. undefined values vanish. And if the object has a circular reference, JSON.stringify throws outright.
structuredClone implements the same structured-clone algorithm browsers use for postMessage and IndexedDB. It knows about Date, Map, Set, RegExp, ArrayBuffer/typed arrays, and circular references natively, because it's not going through a text format at all — it's a direct graph clone.
How it works
Sessionintentionally mixes aDate, aSet, and aMap— exactly the types that break the JSON round-trip — to make the difference concrete rather than theoretical.structuredClone(session)walks the object graph and produces independent copies of every nested structure, not just the top-level object.- Mutating
copy.roles(copy.roles.add("viewer")) after cloning proves the twoSets are genuinely separate instances —original.roles.sizestays at2whilecopy.roles.sizebecomes3.
Gotchas
structuredClonecan't clone functions, DOM nodes, or anything with a prototype chain it doesn't recognize (class instances lose their prototype — you get a plain object back with the same own properties, not an instance of the original class).- It's a genuine deep clone, so it's not free — for very large or deeply nested objects, it costs more than a shallow
{ ...obj }spread. Don't reach for it when a shallow copy would do. - Available as a global since Node 17 — no import needed, but double-check your target runtime if you're supporting anything older.
Related
Object.assign({}, obj)/ spread ({ ...obj }) — shallow copy only; nested objects/arrays are still shared references.Array.prototype.slice()— the array-specific shallow-copy equivalent, same caveat applies to nested items.