@yawaramin
Yeah, this would kind of work.
See, I actually thought of the decorator process as well, which is the reason I looked into PPX. However, I realized it would take me a bit too long to figure out how to write a PPX than the time it was worth during my evaluation of ReScript. Also, (if you cannot tell from my first post) I ultimately think this functionality of updating Objects should be part of the vanilla experience.
So, long story short, the decorator syntax might be better than a keyword as it feels the “ReScript way” to use a decorator.
Counter point, even with the decorator syntax, the situation is still awkward. Although less awkward than what I am doing now.
Note about Object.assign
Object.assign()
is a great idea. I kind of forgot about this function, but actually Immer used this function until 2018 to make the copies you mutate. That got rid of it to support IE 11.
I tried to update my code to use JS.Obj.assign()
.
let updateEmail = (state:{.. "email":string}, update:{.. "email":string}) => {
Js.Obj.assign(state, update)
}
But the return of Js.Obj.assign is {..}
, meaning I lose all context of what the type of return object.
Note from future Js.Obj.assign will not work in ReScript, because it only allows two inputs. You would need to do Js.Obj.assign(Js.Obj.assign({}, state), update))
, However, ReScript does not allow {}
, so you still need to do what I did below.
For my code, I can fix this with some ugliness, but it works😊.
let updateEmail = (state:{.. "email":string}, update:{.. "email":string}) => {
((_:'a, _:'b):'a => {
%raw(`
Object.assign({}, state, update)
`)
})(state, update)
}
However, as far as I understand, decorators can only write more ReScript, not JS. I could be wrong about that. If I am not, that means that Js.Obj.assign()
cannot be used directly, and we need to keep type context with interop tricks.
Solution With Decorator
Assuming there is no language-level change, which again it would be cool if it was just part of the language, I would imagine you would setup the decorator instead so it produces ReScript code like this:
@deriving(accessors)
type name = {"name": string}
@deriving(accessors)
type contact = {"contact": { "email":string, "phone":int }}
// Auto-generated from decorator - would not see in your code
let set_Name = (state: {.. "name": string}, update: {.. "name": string}) => {
((_:'a, _:'b):'a => {
%raw(`
Object.assign({}, state, update)
`)
})(state, update)
}
// Auto-generated from decorator - would not see in your code
let set_Contact = (state: {.. "contact": {.. "email":string, "phone":int }}, update: {.. "contact": {.. "email":string, "phone":int }}) => {
((_:'a, _:'b):'a => {
%raw(`
Object.assign({}, state, update)
`)
})(state, update)
}
This way you can use the call like so
@deriving(accessors)
type contact = {"contact": { "email":string, "phone":int }}
person -> set_Contact(
{
"contact": {"email":"updated@example.com", "phone":person["contact"]["phone"]]}
})
Where person is of type
type person = {
"name":string,
"contact": {
"email":string,
"phone":int,
"address":string
}
}
Summary
So, decorate is a possible solution, but boy is it awkward especially when start talking about complex Objects or having to create one-off types of every update. Compare the above solution to just this:
let updatedState = update(person, (draft)=>{
draft["contacts"]["email"] = "updated@example.com"
})
To @cristianoc point, Objects are just awkward. For example, you cannot compose their types. It is very clear that someone prefers Records. The missing spread syntax to compose Object types means you end up writing more verbose Object types than you may necessarily want. They don’t compose like Record types or object types in TS.