Reputation: 619
Can anyone please explain to me why and how this might happen?
I have a typescript app with Zustand state management.
Somewhere inside the app I am updating certain elements by extracting them from the state and cloning them via a simple Object.Assign
:
let elemToUpdate = Object.assign({},story?.content?.elementsData[nodeId]);
if (elemToUpdate) {
if (elemToUpdate.title) elemToUpdate.title[editorLang] = newName;
else elemToUpdate.title = {[editorLang]:newName} as TextDictionary;
updateElement(nodeId,elemToUpdate);
}
Now the interesting part is: on my first try the update goes through without fail, but the next object I am trying to update fails with the following message:
Tree.tsx:39 Uncaught TypeError: Cannot assign to read only property 'en' of object '#<Object>'
I can't understand WHY the first one comes through, but the second gets blocked.
I know HOW to fix this: I need to do a deep clone. I just want to understand WHY.
Upvotes: 2
Views: 8236
Reputation: 10071
That is expected because in the first case you are not assigning a new value to the title
property: you are only changing a value inside the title
property. In the second case you are reassigning the whole value of the title
property.
You can't change a readonly
property. Let's understand it with a simple example:
Javascript: Only for example not related to problem
const user = { // a const is immutable
name: 'John',
}
user.name = 'Pete'; // This works
const user = {
name: 'John'
}
user = { name: 'Pete'} // This doesn't work
Typescript:
const user: Readonly<{
a: {
name: string
}
}> = {
a:{ name: 'John' }
}
user.a.name = 'Pete'; // This works
user.a = { name: 'John' } // Does not work
The same is happening in your example: Typescript does not check deep Readonly props. Please check here
Upvotes: 1
Reputation: 1635
First, let's start from why some objects in your code are readonly
. Based on what you described in the question, you use a Zustand state manager. Such managers traditionally wraps your stored data to readonly objects to prevent their manual mutation (expecting that you will change the state only via built-in mechanisms) to guarantee data stability. So, if the story?.content?.elementsData[nodeId]
is the Zustand state object, it and all its nested objects are converted to readonly.
Second, let's define, which objects will be blocked. I see at least two objects here: elemToUpdate: { ..., title: { [lang]: string }}
(that is elemToUpdate
and its title
). Both will be converted to readonly.
Third, you use Object.assign({}, ...)
which creates a new object (new reference) and clones all the properties of the source object. Please note that this happens only for the first level of properties, it's not a deep clone. So, as the title
is a reference to another object, it will be cloned as-is and in the new object it will still lead to the existing { [lang]: string }
object. There are several ways to solve that:
title
property, for instance {..., title: { ... elemToUpdate.title }}
or via Object.assignBut I would not suggest you to mutate objects this way. Most probably your entire algorithm has some architectural issues in general.
Upvotes: 4