6

Consider the following code, where line 2 fails with Property 'newProperty' does not exist on type 'WritableDraft<MyObject>'. TS7053

// data is of type MyObject which until now has only a property myNumber
const payload = produce(data, (draft) => {    
  draft['newProperty'] = 'test';              // Property 'newProperty' does not exist on type 'WritableDraft<MyObject>'.  TS7053
});                                           

How can I dynamically add a new property to the draft or change the type of the draft to a type which already includes the newProperty? I do not want to have newProperty in the MyObject type itself.

Linda Paiste
  • 38,446
  • 6
  • 64
  • 102
stefan.at.kotlin
  • 15,347
  • 38
  • 147
  • 270
  • This is not a great solution, but couldn't you just cast it to `any`? – Julia Mar 11 '21 at 20:41
  • Yes, that works, thanks. Yet I am wondering if there is any better / official way to do it? – stefan.at.kotlin Mar 11 '21 at 20:43
  • 1
    https://stackoverflow.com/questions/12710905/how-do-i-dynamically-assign-properties-to-an-object-in-typescript, I pretty sure you might want to take a look at this one. In case if you need to add any new property even in a for loop. This question is quite common in fact but all solutions seem like just defeats the original purpose of typescript. I don't think there is a truly elegant way to do it. Otherwise you just need to add a new property as a interface for the parameter. – Yunhai Mar 11 '21 at 20:44
  • You seem to want to operate like a simple dictionary. The question is ofcourse, what will you do with that object afterwards, and does it make sense (the code you are showing here doesn't offer any benefit to a consumer afterwards, they are not aware it now has new properties, so what is your end goal) – Icepickle Mar 11 '21 at 20:50

3 Answers3

2

If you want to keep this new type on payload you will have to cast new type on parameter data:

let payload = produce(data as MyObject & { newProperty: string }, (draft) => {
  draft["newProperty"] = "test";
})

If you are not interested in keeping your type on payload, you can modify type of draft either in parameter of function, or inside: (draft as MyObject & { newProperty: string })[newProperty] = "test"

Mr. Hedgehog
  • 2,644
  • 1
  • 14
  • 18
1

You can add new properties to preexisting types by using the union operator: &. The code below should solve your problem.

(draft: WritableDraft<MyObject> & { newProperty : string }) => {
  // put the rest of your code here
}

Edit

After looking at Linda Paiste's answer, I think there are a few issues with the approach I took above. My answer does add a property to the type, which removes any error you would get from accessing or modifying the newProperty attribute in the function. However, it is more verbose and difficult to maintain if types or variables names are changed in the future. Additionally, it forces the caller of the function to pass in an object that contains the newProperty property, which is likely not what you want.

Johann
  • 187
  • 1
  • 4
  • 11
1

It's actually fine to return a new object from produce as long as you don't also modify the draft. The docs on returning new data from producers show an example with a switch where some cases modify the draft and others return a new state. That is fine as long as you don't do do both in the same case.

const payload = produce(data, (draft) => {
    return { ...draft, newProperty: "test" };
});

const n = payload.myNumber; // number
const s = payload.newProperty; // string

Pros:

  • No as assertions needed
  • payload gets correct type { newProperty: string; myNumber: number; }

Cons:

  • Doesn't actually make use of Immer's capabilities

Typescript Playground Link

Linda Paiste
  • 38,446
  • 6
  • 64
  • 102