arthur
arthur

Reputation: 619

Cannot assign to read only property of Object in TypeScript

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

Answers (2)

Rahul Sharma
Rahul Sharma

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

Roman Maksimov
Roman Maksimov

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:

  1. deep clone, as you mentioned
  2. manually clone title property, for instance {..., title: { ... elemToUpdate.title }} or via Object.assign

But I would not suggest you to mutate objects this way. Most probably your entire algorithm has some architectural issues in general.

Upvotes: 4

Related Questions