Deep clone and deep freeze for JavaScript — prototype-aware.
Deep clones objects while preserving the prototype chain, property descriptors, boxed primitives, and native types (Map, Set, Date, RegExp, Error subclasses, TypedArray, DataView, ArrayBuffer, Buffer). Cycle-safe via a WeakMap visited cache. Deep-freezes recursively, skipping ArrayBuffer views.
structuredClone ships in every modern runtime. It strips the prototype chain, so class instances come back as plain objects. It drops property descriptors — non-enumerable fields, accessors, and configurable: false flags vanish. Boxed primitives throw. ORM entities, builders, event emitters, frozen state objects, and any custom-constructed value lose information when round-tripped.
@coroboros/clone keeps all three. Three opt-out flags trade those guarantees for speed on plain JSON-shaped data, landing in rfdc-grade territory without switching libraries.
- Node.js
>=22LTS. Use fnm for version management — Rust-based, faster than nvm. - Any of the following package managers:
pnpm,npm,yarn,bun.
pnpm add @coroboros/clonenpm install @coroboros/cloneyarn add @coroboros/clonebun add @coroboros/clone// ESM (recommended)
import { clone, freeze } from '@coroboros/clone';// CommonJS
const { clone, freeze } = require('@coroboros/clone');A deep copy that survives mutation and keeps the prototype:
import { clone, freeze } from '@coroboros/clone';
class Account {
constructor(public id: string, public balance: number) {}
withdraw(amount: number): void {
this.balance -= amount;
}
}
const ledger = {
account: new Account('AC-1', 1000),
audit: new Map([['2026-01-01', true]]),
};
const copy = clone(ledger);
copy.account.withdraw(250);
copy.account.balance; // 750
ledger.account.balance; // 1000 — source untouched
copy.account instanceof Account; // true — method still callable
copy.audit.get('2026-01-01'); // true — a real Map, not {}lodash.cloneDeep copies enumerable values only. clone also carries accessors and non-enumerable keys:
import cloneDeep from 'lodash.clonedeep';
import { clone } from '@coroboros/clone';
const cart = {
items: [{ price: 10 }, { price: 5 }],
get total() {
return this.items.reduce((sum, i) => sum + i.price, 0);
},
};
const kept = clone(cart);
kept.items.push({ price: 100 });
kept.total; // 115 — total is still a live getter
const flat = cloneDeep(cart);
flat.items.push({ price: 100 });
flat.total; // 15 — the getter was frozen to its
// clone-time value; the copy is now stale
const cfg = {};
Object.defineProperty(cfg, 'secret', { value: 'k-1', enumerable: false });
clone(cfg).secret; // 'k-1'
cloneDeep(cfg).secret; // undefined — silently droppedfreeze locks the whole graph. Nested mutation throws in strict mode:
import { clone, freeze } from '@coroboros/clone';
const settled = freeze(clone(ledger));
settled.account.withdraw(50); // TypeError in strict mode; no-op otherwise
settled.account.balance; // 1000
Object.isFrozen(settled.account); // true — recursiveReturns a deep copy of thing. The return type matches the input type via generic inference.
The clone preserves the prototype chain, property descriptors (including non-enumerable, accessor, and configurable: false properties), boxed primitive wrappers, and symbol-keyed properties.
Parameters
| Option | Type | Default | Description |
|---|---|---|---|
thing |
T |
(required) | Value to clone. Any JavaScript value or object. |
options.ignoreUndefinedProperties |
boolean |
false |
When true, omit properties whose value is undefined. Recursive. |
options.cycles |
boolean |
true |
When false, skips the WeakMap visited cache. Caller asserts no cycles. Faster, infinite-recursion if wrong. |
options.preservePrototype |
boolean |
true |
When false, custom objects flatten to plain {} (lose instanceof and method inheritance). |
options.copyDescriptors |
boolean |
true |
When false, plain objects skip Reflect.ownKeys + descriptor walk. Symbol keys and non-enumerable fields drop. Errors keep message + name only; boxed wrappers keep their value only. |
Returns — a deep copy of thing, typed as T.
Native types clone with type-specific semantics:
Array— element-by-element deep clone.Map,Set— keys and values cloned independently.Date— byvalueOf().RegExp—sourceandflagspreserved.TypedArray(Int8ArraythroughFloat64Array) — cloned via the constructor.DataView— buffer copied;byteOffsetandbyteLengthpreserved.Buffer— bytes copied viaBuffer.allocUnsafeandBuffer#copy. Browser bundles skip this branch via a runtime guard; the type is Node-only.ArrayBuffer— sliced.Errorand subclasses (EvalError,RangeError,ReferenceError,SyntaxError,TypeError,URIError) — own properties copied with full descriptors.- Boxed primitives (
new String(),new Number(),new Boolean()) — wrapper recreated with attached properties. - Custom objects — created via
Object.create(getPrototypeOf(source)), then own descriptors applied. - Null-prototype objects (
Object.create(null)) — preserved with the null prototype.
A WeakMap visited cache preserves circular and shared references. Cyclic inputs round-trip correctly:
const o: Record<string, unknown> = { name: 'cyclic' };
o.self = o;
const c = clone(o);
c.self === c; // trueShared references stay shared. A diamond input produces a diamond output, with each shared subtree cloned exactly once.
Composing all three opt-out flags gives a rfdc-grade fast path for callers who know their data is plain and acyclic:
const config = clone(largeJsonConfig, {
cycles: false,
preservePrototype: false,
copyDescriptors: false,
});See bench/baseline.md for the head-to-head numbers vs structuredClone, lodash.cloneDeep, rfdc, and fast-copy.
The following inputs throw CloneError with code: 'UNSUPPORTED_TYPE':
- Functions (sync, async, generator).
Promise.Intl.Collator,Intl.DateTimeFormat,Intl.NumberFormat,Intl.PluralRules.WeakMap,WeakSet.- Constructor functions themselves (e.g.
clone(Array)).
undefined, null, and NaN clone to themselves.
Recursive deep freeze. Walks own properties, freezes each value, then freezes the container. A WeakSet visited cache makes cyclic inputs safe.
Parameters
| Option | Type | Description |
|---|---|---|
thing |
T |
Value to freeze. |
Returns — the same value, frozen, typed as T.
Object.freeze throws on ArrayBufferView instances with elements. freeze leaves the following unfrozen:
- All
TypedArraysubclasses (Int8ArraythroughFloat64Array, plusBigInt64Array/BigUint64Array). DataView.Buffer(aUint8Arraysubclass).
Detection uses ArrayBuffer.isView(thing).
type CloneOptions = {
ignoreUndefinedProperties?: boolean;
cycles?: boolean;
preservePrototype?: boolean;
copyDescriptors?: boolean;
};class CloneError extends Error {
readonly code: CloneErrorCode;
constructor(code: CloneErrorCode, message: string, options?: { cause?: unknown });
}
type CloneErrorCode = 'UNSUPPORTED_TYPE';Inherits from Error. Supports Error.cause for wrapping. The code field is a stable string discriminant safe for runtime branching.
| Feature | structuredClone |
lodash.cloneDeep |
rfdc |
fast-copy |
@coroboros/clone |
|---|---|---|---|---|---|
Native types (Map, Set, Date, TypedArray, ArrayBuffer, DataView) |
yes | yes | partial | yes | yes |
| Cycles | yes | yes | optional | yes | yes |
| Prototype chain preserved | no | partial | no | no | yes |
Property descriptors (non-enumerable, accessor, configurable: false) |
no | no | no | no | yes |
Boxed primitives (new String(), new Number(), new Boolean()) |
throws | partial | no | no | yes |
Error subclasses with descriptors |
partial | no | no | no | yes |
Functions, Promises, WeakMap, WeakSet |
no | no | no | no | no (by design) |
The market gap is the prototype chain plus property descriptors. structuredClone strips the prototype from class instances; they return as plain objects. lodash.cloneDeep drops descriptor flags. ORM entities, builders, event emitters, and any custom-constructed state object stay intact through clone.
Bug reports and PRs welcome.
- Open an issue before submitting non-trivial PRs.
- Commits follow Conventional Commits.
- Run
pnpm lint && pnpm typecheck && pnpm testbefore pushing. - Run
pnpm benchagainstbench/baseline.mdwhen touchingsrc/clone.ts— no regression > 10 % at fixed feature set. - Target the
mainbranch.