Package Exports
- @coroboros/clone
- @coroboros/clone/package.json
Readme
@coroboros/clone
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.
Why this exists
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.
Requirements
- Node.js
>=22LTS. Use fnm for version management — Rust-based, faster than nvm. - Any of the following package managers:
pnpm,npm,yarn,bun.
Install
pnpm add @coroboros/clonenpm install @coroboros/cloneyarn add @coroboros/clonebun add @coroboros/cloneUsage
// ESM (recommended)
import { clone, freeze } from '@coroboros/clone';// CommonJS
const { clone, freeze } = require('@coroboros/clone');import { clone, freeze } from '@coroboros/clone';
class Pet {
constructor(public name: string) {}
greet(): string {
return `hello, ${this.name}`;
}
}
const original = {
pet: new Pet('Saturn'),
history: new Map([['adopted', new Date(2026, 0, 1)]]),
tags: new Set(['cat', 'tabby']),
};
const copy = clone(original);
copy.pet.greet(); // "hello, Saturn"
copy.pet instanceof Pet; // true — prototype preserved
copy.history.get('adopted') instanceof Date; // true
const locked = freeze(copy);
Object.isFrozen(locked); // true
Object.isFrozen(locked.pet); // true — recursiveAPI
clone<T>(thing: T, options?: CloneOptions): T
Returns 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.
Supported types
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.
Cycle handling
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.
Fast clone for plain JSON-shaped data
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.
Unsupported types
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.
freeze<T>(thing: T): T
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.
Skipped types
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).
CloneOptions
type CloneOptions = {
ignoreUndefinedProperties?: boolean;
cycles?: boolean;
preservePrototype?: boolean;
copyDescriptors?: boolean;
};CloneError
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.
Compared to alternatives
| 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.
Contributing
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.