type PlainObject = { [key in string | number | symbol]: unknown } /** * Assigns a prop the copy is keeping, preserving whether it was enumerable. Callers decide *whether* * to keep a non-enumerable prop before getting here, so by this point the answer is always yes. */ function assignProp( carry: PlainObject, key: string | symbol, newVal: any, originalObject: PlainObject, ): void { if (Object.prototype.propertyIsEnumerable.call(originalObject, key)) { carry[key as any] = newVal return } Object.defineProperty(carry, key, { value: newVal, enumerable: false, writable: true, configurable: true, }) } /** * Whether `value` is an object literal, as opposed to a class instance, a built-in like `Date`, an * `arguments` object, or an object with a null prototype. * * The prototype check comes first because it is cheap and rejects almost everything; the tag check * is only reached by values that are about to be cloned anyway. */ function isPlainObject(value: object): boolean { return ( Object.getPrototypeOf(value) === Object.prototype && Object.prototype.toString.call(value) === '[object Object]' ) } /** * Returns the clone that `value` should be replaced with, and queues it to be filled in later. * Anything that isn't an array or a plain object is returned untouched. * * Reusing the clone already registered in `clones` is what makes circular and shared references * work: the placeholder is registered before its contents exist, so a reference back to an ancestor * resolves to it rather than walking forever. */ function cloneRef(value: any, clones: Map, sources: any[], dests: any[]): any { if (typeof value !== 'object' || value === null) return value const array = Array.isArray(value) if (!array && !isPlainObject(value)) return value const existing = clones.get(value) if (existing !== undefined) return existing const clone = array ? new Array(value.length) : {} clones.set(value, clone) sources.push(value) dests.push(clone) return clone } export type Options = { props?: (keyof T)[]; nonenumerable?: boolean } /** * Copy (clone) an object and all its props recursively to get rid of any prop referenced of the * original object. Arrays and the objects inside them are cloned as well. * * Nesting depth is limited only by available memory, and circular references are reproduced in the * copy rather than followed forever. Two props pointing at the same object still point at one * shared copy afterwards. * * @param target Target can be anything * @param [options={}] See type {@link Options} for more details. * * - `{ props: ['key1'] }` will only copy the `key1` property. When using this you will need to cast * the return type manually (in order to keep the TS implementation in here simple I didn't * built a complex auto resolved type for those few cases people want to use this option) * - `{ nonenumerable: true }` will copy all non-enumerable properties. Default is `{}` * * @returns The target with replaced values */ export function copy(target: T, options: Options = {}): T { if (typeof target !== 'object' || target === null) return target const clones = new Map() // An explicit work list rather than recursion, so nesting depth is bounded by the heap instead // of the call stack. Deeply nested input used to throw a RangeError here. const sources: any[] = [] const dests: any[] = [] const result = cloneRef(target, clones, sources, dests) const onlyProps = Array.isArray(options.props) ? options.props : undefined const nonenumerable = options.nonenumerable === true while (sources.length) { const source = sources.pop() const dest = dests.pop() if (Array.isArray(source)) { for (let i = 0; i < source.length; i++) { if (i in source) dest[i] = cloneRef(source[i], clones, sources, dests) } continue } if (nonenumerable) { for (const key of Object.getOwnPropertyNames(source)) { // Skip __proto__ properties to prevent prototype pollution if (key === '__proto__') continue if (onlyProps && !onlyProps.includes(key as any)) continue const newVal = cloneRef(source[key], clones, sources, dests) assignProp(dest, key, newVal, source) } } else { // `for in` skips non-enumerable keys and allocates no key array, which is why it beats // getOwnPropertyNames here. The own check is needed because it also walks the prototype. for (const key in source) { if (!Object.prototype.hasOwnProperty.call(source, key)) continue // Skip __proto__ properties to prevent prototype pollution if (key === '__proto__') continue if (onlyProps && !onlyProps.includes(key as any)) continue dest[key] = cloneRef(source[key], clones, sources, dests) } } for (const key of Object.getOwnPropertySymbols(source)) { if (onlyProps && !onlyProps.includes(key as any)) continue if (!nonenumerable && !Object.prototype.propertyIsEnumerable.call(source, key)) continue const newVal = cloneRef(source[key], clones, sources, dests) assignProp(dest, key, newVal, source) } } return result }