On this page
A copied object can still share most of its data with the original. That surprises developers when editing a draft changes the saved record, or an undo snapshot mysteriously updates itself. The right copying strategy depends on which references must become independent, which value types exist, and how the result will be used.
Inspect the data and define independence
Use the JSON formatter to inspect a sanitized serialized example before deciding how to copy it. Identify nested objects, arrays, and optional fields. Remember that JSON hides important runtime distinctions: a Date may already have become a string, and a Map may not have serialized meaningfully at all.
Define copy depth in terms of behavior. Does the caller only need to reorder a list, change one nested setting, or keep a completely independent snapshot? Those requirements call for different operations. Copying everything is not automatically safer if the application relies on identity, class methods, or shared references within a graph.
Write a minimal failing example before changing production code. Keep the original, perform the intended update on a copy, and assert which original values must remain unchanged. Reference comparisons can then verify why the behavior occurs. More language-focused examples are available in the JavaScript and TypeScript category.
Spread copies one level, not the whole graph
Object spread creates a new ordinary object containing the source's own enumerable properties. Array spread creates a new array from an iterable. Both are shallow copies: nested object values remain references to the same objects. They are excellent tools when that sharing is intentional.
const saved = {
title: "Draft",
settings: { theme: "light" },
};
const shallow = { ...saved };
console.log(shallow !== saved); // true
console.log(shallow.settings === saved.settings); // true
const updated = {
...saved,
settings: { ...saved.settings, theme: "dark" },
};
console.log(saved.settings.theme); // "light"The targeted update copies each container on the path to the changed value. Unchanged branches can remain shared, provided consumers do not mutate them later. This technique is called structural sharing, and it helps equality-based change detection identify what actually changed without duplicating the entire graph.
Spread is not a general class-instance copier. It does not preserve the source prototype, non-enumerable properties, or property descriptors, and reading an enumerable getter can execute code. If an object represents domain behavior rather than plain data, prefer an explicit constructor or copy method that preserves its intended invariants.
See MDN's spread syntax guide for the distinction between iterable spreading and object properties. Similar-looking syntax can conceal different mechanics, especially when spreading arrays into objects or copying values with unusual prototypes.
Use structuredClone for supported snapshots
The browser's structuredClone function recursively copies supported values, including ordinary objects, arrays, Dates, Maps, Sets, and many binary types. It can handle cycles that JSON serialization rejects. It also preserves repeated references within the cloned graph instead of producing unrelated copies for each occurrence.
const original = {
createdAt: new Date("2026-05-11T09:00:00Z"),
tags: new Set(["draft"]),
preferences: { theme: "light" },
};
original.self = original;
const snapshot = structuredClone(original);
snapshot.preferences.theme = "dark";
console.log(original.preferences.theme); // "light"
console.log(snapshot.self === snapshot); // trueThis makes it useful for an independent data snapshot, but not for copying arbitrary application objects. Functions and DOM nodes are not cloneable; unsupported values cause a DataCloneError. Custom prototype chains, accessors, and descriptors are not faithfully recreated as behavioral equivalents. Private class state is another reason not to treat cloning as instance reconstruction.
The structuredClone reference documents supported usage and transfer options. Transfer is not copying: transferring an ArrayBuffer moves its underlying resource and detaches the original buffer. Only use that option when the original owner is intentionally giving up access, such as handing a large buffer to a worker.
Check the runtime support of every environment that executes this code, including tests and server-side utilities. A TypeScript library declaration can make the function type-check without providing the runtime implementation. Do not silently fall back to a different copying strategy unless its changed semantics are acceptable and tested.
Avoid JSON round trips as a universal clone
JSON.parse(JSON.stringify(value)) is a serialization round trip, not a faithful general clone. It can be reasonable when your contract is explicitly JSON-compatible data and serialization itself is the behavior you want to test. It is dangerous when chosen merely because it looks concise.
Dates become strings through their usual JSON representation. Undefined object properties disappear; undefined array elements become null. NaN and Infinity become null. BigInt normally causes serialization to throw, and circular references also fail. Maps and Sets do not become useful equivalent collections without custom serialization logic.
A replacer or reviver can implement a specific wire format, but then you own that format and its ambiguity rules. A string that resembles a timestamp is not necessarily a Date. Read the JSON.stringify reference before building storage or network contracts around its defaults.
Keep serialization boundaries distinct from in-memory copying. An API payload might intentionally flatten richer runtime values into strings and arrays. That is a useful transformation, but naming it clone suggests stronger guarantees than it actually provides. Clear function names prevent downstream callers from assuming preserved types or identity relationships.
Make immutability a discipline, not a clone button
Cloning once does not make the result immutable. Anyone holding the new object can still modify it. Object.freeze prevents certain changes to that object itself, but it is shallow and does not make every nested structure immutable. Freezing a Map object, for example, does not disable its set method.
For ordinary application updates, prefer a targeted update that copies the changed path. For independent snapshots or worker messages, use structured cloning when the data fits its supported model. For domain instances, define explicit reconstruction. These strategies solve different problems rather than competing for one universal recommendation.
Test nested changes, shared references, cycles where relevant, and the exact value types your application stores. Measure large graphs before cloning them during frequent interactions. Repeated deep copies can increase allocation and garbage collection work even when the results are correct. Broader implementation decisions belong alongside your development guidance, with the chosen invariants documented.
Key takeaways
- Spread creates a new container while sharing nested references.
- Copy each changed path when making targeted immutable updates.
- Use structuredClone for supported, independent data snapshots.
- Treat JSON conversion as serialization with explicit type losses.
- Test reference relationships as well as visible values.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor