import { isArray } from "@/helpers/is-array.ts";
import { isMap } from "@/helpers/is-map.ts";
import { isObject } from "@/helpers/is-object.ts";
import { isSet } from "@/helpers/is-set.ts";
* A strategy for cloning objects with `cloneDeep`.
* Methods **must** call the `track` function with the new parent
* object **before** looping over the input object's
* properties/elements for cloning purposes. This protects against
* Methods may return the input object to indicate that cloning should
* Methods may return null to indicate that the default cloning logic
export interface CloningStrategy {
track: (newParent: Map<K, V>) => Map<K, V>,
clone: <T>(value: T) => T,
track: (newParent: Set<T>) => Set<T>,
clone: <T>(value: T) => T,
track: (newParent: T[]) => T[],
clone: <T>(value: T) => T,
cloneObject: <T extends object>(
track: (newParent: T) => T,
clone: <T>(value: T) => T,
track: (newParent: T) => T,
clone: <T>(value: T) => T,
export const DefaultCloningStrategy: CloningStrategy = {
track: (newParent: Map<K, V>) => Map<K, V>,
clone: <T>(value: T) => T,
const output = track(new Map());
for (const [key, value] of input) {
output.set(key, clone(value));
track: (newParent: Set<T>) => Set<T>,
clone: <T>(value: T) => T,
const output = track(new Set());
for (const value of input) {
output.add(clone(value));
track: (newParent: T[]) => T[],
clone: <T>(value: T) => T,
// Use .forEach for correct handling of sparse arrays
const output = track(new Array(input.length));
input.forEach((value, index) => {
output[index] = clone(value);
cloneObject<T extends object>(
track: (newParent: T) => T,
clone: <T>(value: T) => T,
const output = track(Object.create(Object.getPrototypeOf(input)));
for (const key of Reflect.ownKeys(input)) {
// By copying the property descriptors, we preserve computed
// properties and non-enumerable properties.
const descriptor = Object.getOwnPropertyDescriptor(input, key)!;
if ("value" in descriptor) {
descriptor.value = clone(descriptor.value);
Object.defineProperty(output, key, descriptor);
cloneOther<T>(input: T, track: (newParent: T) => T): T {
* If you don't need support for non-enumerable properties or computed
* properties, and you're not using custom classes, you can use this
* strategy for better performance.
export const FastCloningStrategy = {
cloneObject: <T extends object>(
track: (newParent: T) => T,
clone: <T>(value: T) => T,
const output: any = track({ ...input });
for (const key of Object.keys(input)) {
output[key] = clone(input[key as keyof object]);
* Clone the given object and possibly other objects nested inside.
* By default, the only objects that get cloned are plain objects,
* class instances, arrays, `Set` instances, and `Map` instances. If
* an object is not cloned, any objects nested inside are also not
* You may define a custom cloning strategy by passing a partial
* implementation of the `CloningStrategy` interface to the
* `cloneDeep` function. Any undefined methods will fall back to the
* default cloning logic. Your own methods may return null to indicate
* that the default cloning logic should be used. They may also return
* the input object to indicate that cloning should be skipped.
* const obj = { a: 1, b: { c: 2 } }
* const clone = cloneDeep(obj)
* assert(clone.b !== obj.b)
* assert(JSON.stringify(clone) === JSON.stringify(obj))
export function cloneDeep<T extends object>(
customStrategy?: Partial<CloningStrategy>,
const strategy = { ...DefaultCloningStrategy, ...customStrategy };
const tracked = new Map<unknown, unknown>();
const track = (parent: unknown, newParent: unknown) => {
tracked.set(parent, newParent);
const clone = <T,>(value: T): T =>
value && typeof value === "object"
? ((tracked.get(value) ?? cloneDeep(value, strategy)) as T)
const cloneDeep = (parent: unknown, strategy: CloningStrategy): unknown => {
track: (newParent: unknown) => unknown,
clone: (value: unknown) => unknown,
const newParent = cloneParent(parent, track.bind(null, parent), clone);
// Use the default strategy if null is returned.
return cloneDeep(parent, DefaultCloningStrategy);
tracked.set(parent, newParent);
return cloneDeep(root, strategy) as T;