@golden-ants/clone
A new approach to deep cloning. A simple library with no external dependencies gives you the opportunity to customize the cloning strategy.
Table of Contents
Installing
Using npm:
npm install --save @golden-ants/clone
Using yarn:
yarn add @golden-ants/clone
Usage
Create an instance of the Clone
class by passing switchers
to it.
import { Clone, Switcher } from '@golden-ants/clone'
const switchers: Switcher[] = [
{
// all primitive types
if: (value) => value === null || typeof value !== 'object',
// just return the same value
handler: (value) => value,
},
{
// all non-primitive types
if: (value) => value !== null && typeof value === 'object',
then: [
{
if: (value) => value instanceof Array,
// copy a nested array using the same switchers
handler: (value, clone) => clone.it([], value as object),
},
{
if: (value) => value instanceof Object,
// copy a nested object using the same switchers
handler: (value, clone) => clone.it({}, value as object),
},
],
},
]
export const deepClone = new Clone(switchers)
Note: The alternative is to use functional utilities
Declarative solution:
const switchers: Switcher[] = [
{
if: not(isObject),
handler: (value) => value,
},
{
if: isObject,
then: [
{
if: instanceOf(Array),
handler: retryFor(() => []),
},
{
if: instanceOf(Object),
handler: retryFor(() => ({})),
},
],
},
]
Calling the created deepClone
for deep recursive copying of objects or arrays.
Note: The target of copying can be an
array
or anobject
, but array elements or object property keys can only be those types that meet the conditions of theswitchers
.
import { deepClone } from ...
// array (object) of any nesting level
const array = [1, [2], { a: [4, 5] }];
const copied = deepClone.it([], array);
Switcher
Switchers are needed to analyze the contents of objects and apply unique ways of processing values.
Each Switcher contains an if
property that accepts a predicate function, the second property can be either a handler or an array of nested switches.
const switcher: Switcher[] = [
{
// all primitives except null
if: (value) => typeof value !== 'object',
/**if the condition is satisfied and the "then" property
* is defined, the value will be passed from top to bottom
* until the result of the comparison is true, otherwise
* an exception will be thrown */
then: [
{
if: (value) => typeof value !== 'string',
handler: (value) => (value as string).toUpperCase(),
},
{
// We repeated the upper condition to avoid an exception
if: (value) => typeof value !== 'object',
handler: (value) => value,
},
],
},
/**If an array or object contains a non-primitive
* value or null, and the corresponding switch is
* not declared, an error will be thrown */
//{
// if: (value) => typeof value === "object",
// handler: (value) => ...
//}
]
Warning: If you define the
then
andhandler
properties in an Switcher, the consequences will be unpredictable
Clone
An instance of the Clone
class contains a single public member - the it
method.
Warning: Method arguments can only be non-null objects.
Cloning occurs superficially, a handler
is applied to each property of the source
, the result of which is assigned to the target
. But thanks to the hook, you can call the cloning again, but for a nested object.
const clone = new Clone(...)
const target = {}
const source = { foo: "bar" }
const copied = clone.it(target, source)
copied === target // true
copied === source // false
// clone.it(317, source) // runtime error
And so the handler
can cause cloning for a nested object.
...{
if: isObject,
/**value: object property value
* clone: an instance of the Clone class to which these switchers will be passed*/
handler: (value, clone) => clone.it({}, value),
}...
Functional utilities
instanceOf
Higher order function.
class MyClass {}
const isMyClass = instanceOf(MyClass)
const mc = new MyClass()
isMyClass(mc) // true
isObject
Checks whether the value is not primitive.
isObject('clone') // false
isObject(null) // false
isObject({}) // true
isObject(Object.create(null)) // true
retryFor
A higher-order function accepting a supplier.
...{
if: (value) => value instanceof Array,
handler: (value, clone) => clone.it([], value as object),
}...
It's clearer this way.
...{
if: instanceOf(Array),
handler: retryFor(() => []),
}...
not
A higher-order function, returns a function through the result will be inverted.
const isPrimitive = not(isObject)
isPrimitive(1)) //true
isPrimitive(null)) //true
Examples
Shallow copy
import { Clone, Switcher } from '@golden-ants/clone'
const switchers: Switcher[] = [
{
if: () => true,
handler: (value) => value,
},
]
const shallowCopy = new Clone(switchers)
const source = {
foo: {
bar: 'abc',
},
}
const target: Record<string, unknown> = {}
shallowCopy.it(target, source)
console.log(target.foo === source.foo) // true
Deep copy
import {
Clone,
instanceOf,
isObject,
not,
retryFor,
Switcher,
} from '@golden-ants/clone'
const switchers: Switcher[] = [
{
if: not(isObject),
handler: (value) => value,
},
{
if: isObject,
then: [
{
if: instanceOf(Array),
handler: retryFor(() => []),
},
{
if: instanceOf(Object),
handler: retryFor(() => ({})),
},
],
},
]
const deepCopy = new Clone(switchers)
const source = {
foo: {
bar: 'abc',
},
}
const target: Record<string, unknown> = {}
deepCopy.it(target, source)
console.log(target.foo === source.foo) // false
Custom copy
import {
Clone,
instanceOf,
isObject,
not,
retryFor,
Switcher,
} from '@golden-ants/clone'
class A {
private storage: string
constructor(storage: string) {
this.storage = storage
}
clone = () => new A(this.storage + ' clone A')
get = () => this.storage
}
const switchers: Switcher[] = [
{
if: not(isObject),
handler: (value) => value,
},
{
if: isObject,
then: [
{
if: instanceOf(A),
handler: (value) => (value as A).clone(),
},
{
if: instanceOf(Array),
handler: retryFor(() => []),
},
{
if: instanceOf(Object),
handler: retryFor(() => ({})),
},
],
},
]
const customCopy = new Clone(switchers)
const source = [new A('foo'), new A('bar')]
const target: A[] = []
customCopy.it(target, source)
target[0].get() // 'foo clone A'
target[1].get() // 'bar clone A'