javascriptreactjstypescriptlodashdeep-copy

Strange behavior when deep cloning dynamic objects


I have been trying to deep clone a dynamic object in typescript and all methods just return an empty {} object for this specific scenario!!

Here is the type of object I am trying to deep clone

fullValues: { [key : string ] : Array<string> },

NOTE: fullValues is passed to a react component and the below mentioned operations happen in this react component! fullValues is NEVER directly mutated throughout the lifecycle of the program and it is initially a state in the parent component as shown below:

const facetValues: { [key: string ] : Array<string> } = {};

// Type => facetedData?: FacetCollectionType
if (facetedData) {
    Object.entries(facetedData).forEach(([key, value]) => {
        Object.defineProperty(facetValues, key, { value: [] as string[]});
    });
}

const [ facets, setFacets ] = useState<{ [key: string ] : Array<string> }>(facetValues);


{facetedData && 
                            
    Object.keys(facetedData).length !== 0 ?
        Object.entries(facetedData).map(([key, options]) => (
            <DataTableFacetedFilter
                key={key}
                options={options}
                mainKey={key}
                title={key}
                fullValues={facets}
                setSelectedValues={setFacets}
            />
        ))
        :
        null
}

Random example of how this object can be structured:

{
   status: [],
   plan: [],
}

I tried the following methods for deepcloning:

Using lodash deepclone

console.log(fullValues); // outputs { status: [], plan: [] }
console.log("after deep clone => ");
console.log(_cloneDeep(fullValues)); // outputs {}

Using JSON stringify method

console.log(fullValues); // outputs { status: [], plan: [] }
console.log("after deep clone => ");
console.log(JSON.parse(JSON.stringify(fullValues))); // outputs {}

However if I do this

let fullValues: { [key : string ] : Array<string> }  = { status: [], plan: [] };
console.log(fullValues); // outputs { status: [], plan: [] }
console.log("after deep clone => ");
console.log(_cloneDeep(fullValues)); // outputs { status: [], plan: [] }

It works here.

There seems to be no logic to why this is happening? It makes no sense!


Solution

  • This stems from the use of Object.defineProperty to set the fields.

    You can reduce it to this simplified example.

    const values : { [key: string]: Array<string> } = {};
    console.log(`1: ${JSON.stringify(values)}`); // 1: {}
    Object.defineProperty(values, 'status', { value: 'ok' });
    console.log(`2: ${JSON.stringify(values.status)}`); // 2: "ok"
    console.log(`3: ${JSON.stringify(values)}`); // 3: {}

    According to the MDN documentation for defineProperty:

    By default, properties added using Object.defineProperty() are not writable, not enumerable, and not configurable.

    By being non-enumerable, these methods for cloning the object do not see these properties. Assuming that defineProperty was used deliberately to make the properties non-writable, you can make them explicitly enumerable:

    const values : { [key: string]: Array<string> } = {};
    console.log(`1: ${JSON.stringify(values)}`); // 1: {}
    Object.defineProperty(values, 'status', { value: 'ok', enumerable: true });
    console.log(`2: ${JSON.stringify(values.status)}`); // 2: "ok"
    console.log(`3: ${JSON.stringify(values)}`); // 3: {"status":"ok"}

    If making the properties non-writable and non-configurable isn't required, a simpler solution is to use an indexed assignment:

    const values : { [key: string]: Array<string> } = {};
    console.log(`1: ${JSON.stringify(values)}`); // 1: {}
    const key = 'status';
    values[key] = 'ok'; // `values.status = 'ok'` would also work for non-variable keys
    console.log(`2: ${JSON.stringify(values.status)}`); // 2: "ok"
    console.log(`3: ${JSON.stringify(values)}`); // 3: {"status":"ok"}