# Deep clone object - JSON.stringify/parse, fast-copy, structuredClone

Everyone, at a certain point in a developer's career, is going to search the internet for "how to make a deep copy of an object". I'll focus on "data objects" which can be defined by:

* no methods, setters or getters, only properties
    
* properties can be nested (arrays, objects)
    
* all properties are "cloneable", i.e. they can't be `Error`, `Promise`, `Function`, `undefined` and some other types
    
* no circular references
    

I'll show three ways to do so. There are of course hundreds of other ways but these three should be more than enough for data objects. In the end, I'll write some tests in Jest to verify the objects' equality.

# Setup

Requirements:

* node 17
    
* npm 8
    
* npx
    

Initialize **npm** with default settings

```bash
npm init -y
```

Install dependencies

```bash
npm i jest@29 @types/jest@29 ts-jest@29 typescript@5 fast-copy
```

Initialize the default configuration of **ts-jest**

```bash
npx ts-jest config:init
```

# Simple examples

## JSON.stringify, JSON.parse

Documentation:

* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global\_Objects/JSON/stringify
    
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global\_Objects/JSON/parse
    

These methods do exactly what the names suggest. First, we call `JSON.stringify()` method that converts a value to plain JSON format (the returned value is a string). Then we call `JSON.parse()` method to parse the JSON string into a plain javascript object. In this process, all the references and class types are lost. In terms of speed that's the slowest solution and should only be used when performance is not a big deal.

Usage:

```typescript
const variable = { myProp: [1, 'two'] };
console.log('original:', variable);
const clone = JSON.parse(JSON.stringify(variable));
console.log('cloned:  ', clone);
```

## fast-copy

Documentation: https://github.com/planttheidea/fast-copy

This library has a few methods, but I'll focus on `copy()`. This method takes a single argument and returns a cloned value. One of the advantages this library has is that it tries to retain the source class type which the other two libraries don't.

Usage

```typescript
import copy from 'fast-copy';

const variable = { myProp: [1, 'two'] };
console.log('original:', variable);
const clone = copy(variable)
console.log('cloned:  ', clone);
```

## structuredClone

Documentation: https://developer.mozilla.org/en-US/docs/Web/API/structuredClone

This built-in function is available only in the newest browsers (https://caniuse.com/?search=structuredClone) and by default in Node 17+. Its usage is very simple: just call the function and pass a value to clone it.

```typescript
const variable = { myProp: [1, 'two'] };
console.log('original:', variable);
const clone = structuredClone(variable)
console.log('cloned:  ', clone);
```

## The result

All the previous examples were quite simple. Each time the result should be the same:

```text
original: { myProp: [ 1, 'two' ] }
cloned:   { myProp: [ 1, 'two' ] }
```

# Complex example

Create an example to check multiple values

```typescript
// index.js 

import copy from 'fast-copy';

class TestClass {
  number: number;
  string: string;
  array: unknown[];
  object: object;
  class: TestClass;
}

const simpleObject = { propNumber: 256, propString: 'a string prop' };

const otherTestClass = new TestClass();
otherTestClass.array = [2, '3', 'five', [], simpleObject, null, undefined];
otherTestClass.string = 'a new string';
otherTestClass.number = -88;
otherTestClass.object = { prop2: 'val2' };

const myTestClass = new TestClass();
myTestClass.array = [1, '2', 'three', [], simpleObject, null, undefined];
myTestClass.string = 'a string';
myTestClass.number = 99;
myTestClass.object = { prop: 'val' };
myTestClass.class = otherTestClass;

const values = [
  { key: 'null', value: null },
  { key: 'false', value: false },
  { key: 'true', value: true },
  { key: '0', value: 0 },
  { key: '-10', value: -10 },
  { key: '99', value: 99 },
  { key: '0.1', value: 0.1 },
  { key: '-1.0009', value: -1.0009 },
  { key: 'Infinity', value: Infinity },
  { key: '\'\'', value: '' },
  { key: 'a string', value: 'a string' },
  { key: '[]', value: [] },
  { key: '[1,\'2\',\'three\', [], {prop: \'val\'}]', value: [1, '2', 'three', [], { prop: 'val' }] },
  { key: '{}', value: {} },
  { key: 'myTestClass', value: myTestClass }
]

class Result {

  constructor(input: any, jsonStringifyParse: any, fastCopy: any, structuredClone: any) {
    this.input = input;
    this.jsonStringifyParse = jsonStringifyParse;
    this.fastCopy = fastCopy;
    this.structuredClone = structuredClone;
  }

  input: any;
  jsonStringifyParse: any;
  fastCopy: any;
  structuredClone: any;
}

const group = {};

values.forEach(value => {
  group[value.key] = new Result(
    value.value,
    JSON.parse(JSON.stringify(value.value)),
    copy(value.value),
    structuredClone(value.value)
  );
})

console.table(group, ['jsonStringifyParse', 'fastCopy', 'structuredClone'])
console.dir(group, {depth: null})
```

Run it

```shell
npx tsc index.ts && node index.js
```

The output of `console.table()`

```shell
┌────────────────────────────────────┬───────────────────────────────────────┬───────────────────────────────────────┬───────────────────────────────────────┐
│              (index)               │          jsonStringifyParse           │               fastCopy                │            structuredClone            │
├────────────────────────────────────┼───────────────────────────────────────┼───────────────────────────────────────┼───────────────────────────────────────┤
│                 0                  │                   0                   │                   0                   │                   0                   │
│                 99                 │                  99                   │                  99                   │                  99                   │
│                null                │                 null                  │                 null                  │                 null                  │
│               false                │                 false                 │                 false                 │                 false                 │
│                true                │                 true                  │                 true                  │                 true                  │
│                -10                 │                  -10                  │                  -10                  │                  -10                  │
│                0.1                 │                  0.1                  │                  0.1                  │                  0.1                  │
│              -1.0009               │                -1.0009                │                -1.0009                │                -1.0009                │
│              Infinity              │                 null                  │               Infinity                │               Infinity                │
│                 ''                 │                  ''                   │                  ''                   │                  ''                   │
│              a string              │              'a string'               │              'a string'               │              'a string'               │
│                 []                 │                  []                   │                  []                   │                  []                   │
│ [1,'2','three', [], {prop: 'val'}] │ [ 1, '2', 'three', ... 2 more items ] │ [ 1, '2', 'three', ... 2 more items ] │ [ 1, '2', 'three', ... 2 more items ] │
│                 {}                 │                  {}                   │                  {}                   │                  {}                   │
│            myTestClass             │               [Object]                │              [TestClass]              │               [Object]                │
└────────────────────────────────────┴───────────────────────────────────────┴───────────────────────────────────────┴───────────────────────────────────────┘
```

A quick look can tell if the values match and where the class types were retained. Since `console.table()` does not expand objects, the `console.dir()` can be used. To expand variables without the nesting limit pass `{depth: null}` as the second argument.

The output of `console.dir()`

```shell
{
  '0': Result {
    input: 0,
    jsonStringifyParse: 0,
    fastCopy: 0,
    structuredClone: 0
  },
  '99': Result {
    input: 99,
    jsonStringifyParse: 99,
    fastCopy: 99,
    structuredClone: 99
  },
  null: Result {
    input: null,
    jsonStringifyParse: null,
    fastCopy: null,
    structuredClone: null
  },
  false: Result {
    input: false,
    jsonStringifyParse: false,
    fastCopy: false,
    structuredClone: false
  },
  true: Result {
    input: true,
    jsonStringifyParse: true,
    fastCopy: true,
    structuredClone: true
  },
  '-10': Result {
    input: -10,
    jsonStringifyParse: -10,
    fastCopy: -10,
    structuredClone: -10
  },
  '0.1': Result {
    input: 0.1,
    jsonStringifyParse: 0.1,
    fastCopy: 0.1,
    structuredClone: 0.1
  },
  '-1.0009': Result {
    input: -1.0009,
    jsonStringifyParse: -1.0009,
    fastCopy: -1.0009,
    structuredClone: -1.0009
  },
  Infinity: Result {
    input: Infinity,
    jsonStringifyParse: null,
    fastCopy: Infinity,
    structuredClone: Infinity
  },
  "''": Result {
    input: '',
    jsonStringifyParse: '',
    fastCopy: '',
    structuredClone: ''
  },
  'a string': Result {
    input: 'a string',
    jsonStringifyParse: 'a string',
    fastCopy: 'a string',
    structuredClone: 'a string'
  },
  '[]': Result {
    input: [],
    jsonStringifyParse: [],
    fastCopy: [],
    structuredClone: []
  },
  "[1,'2','three', [], {prop: 'val'}]": Result {
    input: [ 1, '2', 'three', [], { prop: 'val' } ],
    jsonStringifyParse: [ 1, '2', 'three', [], { prop: 'val' } ],
    fastCopy: [ 1, '2', 'three', [], { prop: 'val' } ],
    structuredClone: [ 1, '2', 'three', [], { prop: 'val' } ]
  },
  '{}': Result {
    input: {},
    jsonStringifyParse: {},
    fastCopy: {},
    structuredClone: {}
  },
  myTestClass: Result {
    input: TestClass {
      array: [
        1,
        '2',
        'three',
        [],
        { propNumber: 256, propString: 'a string prop' },
        null,
        undefined
      ],
      string: 'a string',
      number: 99,
      object: { prop: 'val' },
      class: TestClass {
        array: [
          2,
          '3',
          'five',
          [],
          { propNumber: 256, propString: 'a string prop' },
          null,
          undefined
        ],
        string: 'a new string',
        number: -88,
        object: { prop2: 'val2' }
      }
    },
    jsonStringifyParse: {
      array: [
        1,
        '2',
        'three',
        [],
        { propNumber: 256, propString: 'a string prop' },
        null,
        null
      ],
      string: 'a string',
      number: 99,
      object: { prop: 'val' },
      class: {
        array: [
          2,
          '3',
          'five',
          [],
          { propNumber: 256, propString: 'a string prop' },
          null,
          null
        ],
        string: 'a new string',
        number: -88,
        object: { prop2: 'val2' }
      }
    },
    fastCopy: TestClass {
      array: [
        1,
        '2',
        'three',
        [],
        { propNumber: 256, propString: 'a string prop' },
        null,
        undefined
      ],
      string: 'a string',
      number: 99,
      object: { prop: 'val' },
      class: TestClass {
        array: [
          2,
          '3',
          'five',
          [],
          { propNumber: 256, propString: 'a string prop' },
          null,
          undefined
        ],
        string: 'a new string',
        number: -88,
        object: { prop2: 'val2' }
      }
    },
    structuredClone: {
      array: [
        1,
        '2',
        'three',
        [],
        { propNumber: 256, propString: 'a string prop' },
        null,
        undefined
      ],
      string: 'a string',
      number: 99,
      object: { prop: 'val' },
      class: {
        array: [
          2,
          '3',
          'five',
          [],
          { propNumber: 256, propString: 'a string prop' },
          null,
          undefined
        ],
        string: 'a new string',
        number: -88,
        object: { prop2: 'val2' }
      }
    }
  }
}
```

Now, you can visually compare all the outputs and tell if that's the desired output.

# Testing

Nevertheless, a solid test case is surely needed. I'll use the same input data.

```typescript
// index.spec.ts

import copy from 'fast-copy';

class TestClass {
  number: number;
  string: string;
  array: unknown[];
  object: object;
  class: TestClass;
}

const simpleObject = { propNumber: 256, propString: 'a string prop' };

const otherTestClass = new TestClass();
otherTestClass.array = [2, '3', 'five', [], simpleObject, null, undefined];
otherTestClass.string = 'a new string';
otherTestClass.number = -88;
otherTestClass.object = { prop2: 'val2' };

const myTestClass = new TestClass();
myTestClass.array = [1, '2', 'three', [], simpleObject, null, undefined];
myTestClass.string = 'a string';
myTestClass.number = 99;
myTestClass.object = { prop: 'val' };
myTestClass.class = otherTestClass;

const values = [
  { key: 'null', value: null },
  { key: 'false', value: false },
  { key: 'true', value: true },
  { key: '0', value: 0 },
  { key: '-10', value: -10 },
  { key: '99', value: 99 },
  { key: '0.1', value: 0.1 },
  { key: '-1.0009', value: -1.0009 },
  { key: 'Infinity', value: Infinity },
  { key: '\'\'', value: '' },
  { key: 'a string', value: 'a string' },
  { key: '[]', value: [] },
  { key: '[1,\'2\',\'three\', [], {prop: \'val\'}]', value: [1, '2', 'three', [], { prop: 'val' }] },
  { key: '{}', value: {} },
  { key: 'myTestClass', value: myTestClass }
]

const jsonStringifyParseDatasets = values.map(value => {
  return {
    key: value.key,
    input: value.value,
    result: JSON.parse(JSON.stringify(value.value))
  };
});

const fastCopyDatasets = values.map(value => {
  return {
    key: value.key,
    input: value.value,
    result: copy(value.value)
  };
});


const structuredCloneDatasets = values.map(value => {
  return {
    key: value.key,
    input: value.value,
    result: structuredClone(value.value)
  };
});

describe('jsonStringifyParse', () => {
  it.each(jsonStringifyParseDatasets)('value: $key', ({key, input, result}) => {
    expect(result).toEqual(input)
  });
});

describe('fastCopy', () => {
  it.each(fastCopyDatasets)('value: $key', ({key, input, result}) => {
    expect(result).toEqual(input)
  });
});

describe('structuredClone', () => {
  it.each(structuredCloneDatasets)('value: $key', ({key, input, result}) => {
    expect(result).toEqual(input)
  });
});
```

Run it

```bash
npx jest index.spec.ts
```

The result is

```plaintext
 FAIL  ./index.spec.ts
  jsonStringifyParse
    ✓ value: null (2 ms)
    ✓ value: false
    ✓ value: true (1 ms)
    ✓ value: 0
    ✓ value: -10
    ✓ value: 99
    ✓ value: 0.1
    ✓ value: -1.0009 (1 ms)
    ✕ value: Infinity (1 ms)
    ✓ value: '' (1 ms)
    ✓ value: a string
    ✓ value: [] (1 ms)
    ✓ value: [1,'2','three', [], {prop: 'val'}]
    ✓ value: {} (1 ms)
    ✕ value: myTestClass (5 ms)
  fastCopy
    ✓ value: null
    ✓ value: false (1 ms)
    ✓ value: true
    ✓ value: 0
    ✓ value: -10
    ✓ value: 99
    ✓ value: 0.1
    ✓ value: -1.0009
    ✓ value: Infinity
    ✓ value: '' (1 ms)
    ✓ value: a string
    ✓ value: []
    ✓ value: [1,'2','three', [], {prop: 'val'}]
    ✓ value: {} (1 ms)
    ✓ value: myTestClass
  structuredClone
    ✓ value: null
    ✓ value: false (1 ms)
    ✓ value: true
    ✓ value: 0
    ✓ value: -10
    ✓ value: 99
    ✓ value: 0.1
    ✓ value: -1.0009
    ✓ value: Infinity
    ✓ value: ''
    ✓ value: a string (1 ms)
    ✓ value: []
    ✓ value: [1,'2','three', [], {prop: 'val'}]
    ✓ value: {}
    ✓ value: myTestClass (1 ms)

  ● jsonStringifyParse › value: Infinity

    expect(received).toEqual(expected) // deep equality

    Expected: Infinity
    Received: null

      69 | describe('jsonStringifyParse', () => {
      70 |   it.each(jsonStringifyParseDatasets)('value: $key', ({key, input, result}) => {
    > 71 |     expect(result).toEqual(input)
         |                    ^
      72 |   });
      73 | });
      74 |

      at index.spec.ts:71:20

  ● jsonStringifyParse › value: myTestClass

    expect(received).toEqual(expected) // deep equality

    - Expected  - 4
    + Received  + 4

    @@ -1,30 +1,30 @@
    - TestClass {
    + Object {
        "array": Array [
          1,
          "2",
          "three",
          Array [],
          Object {
            "propNumber": 256,
            "propString": "a string prop",
          },
    +     null,
          null,
    -     undefined,
        ],
    -   "class": TestClass {
    +   "class": Object {
          "array": Array [
            2,
            "3",
            "five",
            Array [],
            Object {
              "propNumber": 256,
              "propString": "a string prop",
            },
            null,
    -       undefined,
    +       null,
          ],
          "number": -88,
          "object": Object {
            "prop2": "val2",
          },

      69 | describe('jsonStringifyParse', () => {
      70 |   it.each(jsonStringifyParseDatasets)('value: $key', ({key, input, result}) => {
    > 71 |     expect(result).toEqual(input)
         |                    ^
      72 |   });
      73 | });
      74 |

      at index.spec.ts:71:20

Test Suites: 1 failed, 1 total
Tests:       2 failed, 43 passed, 45 total
```

As you can see the 2 tests failed while using `json stringify / parse` method:

* the `undefined` value was converted to null
    
* the `Infinity` value was converted to null
    
* the `TestClass` type was converted back to `Object` type
    

These errors are caused due to the lack of support of these types by JSON format.
