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!
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"}