DEV Community

Yawar Amin
Yawar Amin

Posted on

Immutably updating JavaScript objects in ReasonML (BuckleScript)

BUCKLESCRIPT provides a type-safe OCaml programming environment that allows us to output and deploy JavaScript. ReasonML provides some syntax sugar to make working with JavaScript objects a bit nicer. However, it doesn't sugar over everything we can do with objects in JavaScript, like object spread syntax.

In JavaScript, you can use object spread syntax to immutably update objects:

const bob = {id: 1, name: "Bob", age: 34};
const bob2 = {...bob, age: 35};
Enter fullscreen mode Exit fullscreen mode

In Reason, there is no object spread syntax, but you can do it manually:

let bob = {"id": 1, "name": "Bob", "age": 34};
let bob2 = {"id": bob##id, "name": bob##name, "age": 35};
Enter fullscreen mode Exit fullscreen mode

But this is quite manual. There must be a more elegant way!

Well–in JavaScript before the advent of object spread, people used the Object.assign method. And BuckleScript ships with a binding Js.Obj.assign, to exactly that method. This binding is effectively just a normal function from the Reason/OCaml point of view. So we can build a convenient, functional-style 'object spread' on top of it!

Here's the update function:

// JsObj.re

/** [update(~props, obj)] returns a shallow copy of [obj] updated with
    [props]. */
let update = (~props, obj) =>
  Js.Obj.(()->empty->assign(obj)->assign(props));
Enter fullscreen mode Exit fullscreen mode

This effectively does an immutable update of obj with the given props. The input obj is unchanged. Perfect for React state updates, for example! Here's an example usage:

let bob = {"id": 1, "name": "Bob", "age": 34};
let bob2 = update(~props={"age": 35}, bob);

Js.log2("bob:", bob);
Js.log2("bob2:", bob2);
Enter fullscreen mode Exit fullscreen mode

If you're familiar with JavaScript's Object.assign, you probably already understand how this works. If not, short explanation: it allocates a new, empty object, then shallow-copies all the props from obj into the new object, then sets the new props on the new object–leaving the old obj alone. The new props are also provided in the form of a normal object.

So, does this cover all the use cases of JavaScript's object spread? Probably not! But it's a demonstration of how Reason can have quite elegant, functional-style equivalents to JavaScript idioms.

Top comments (8)

Collapse
 
hoichi profile image
Sergey Samokhov

I wish there was a way to limit props to a subset of the obj fields in a generic way (trivial in TypeScript, by the way). Otherwise, you can easily pass "agw" and spend some time debugging why age fails to update.

Collapse
 
yawaramin profile image
Yawar Amin

Good point! Hopefully this becomes less of a pain once records-as-objects is shipped and we can start modelling JS objects as Reason records with immutable update. Assuming they give the OK to use it like that!

Collapse
 
hoichi profile image
Sergey Samokhov

Oh. I thought that maybe things like that can be amended via some PPX, but if there’s a first-class support forthcoming, so much the better.

Thread Thread
 
yawaramin profile image
Yawar Amin • Edited

It could be handled with a PPX too. E.g. the [@bs.deriving abstract] annotation generates getters and setters for the object type it annotates: bucklescript.github.io/docs/en/obj... . Theoretically it could be extended to also generate a 'copy constructor':

[@bs.deriving abstract]
type person = {id: int, name: string};

// Generate:

let copy = (person, ~id=person##id, ~name=person##name, ()) =>
  {"id": id, "name": name};

// Usage:

let bob = person(~id=1, ~name="Bob");
let bob2 = bob->copy(~id=2, ());
Thread Thread
 
hoichi profile image
Sergey Samokhov

Well, if I correctly understand what record-as-objects is (namely, compiling ML records to JS objects as opposed to JS arrays), your ‘copy constructors’ (or assign constructors or what have you) could still be useful, provided one needs do deal with optional object properties. I mean, you would still probably need [@bs.derivig abstract] if some of your properties are optional, no?

Thread Thread
 
yawaramin profile image
Yawar Amin

If the properties themselves are optional, true! That throws another thorn into the copy constructor generator idea.

Collapse
 
austindd profile image
Austin

This is really useful. I have been using this pattern for a little while now. But I do want to point out that Object.assign (the compiled JS function) is not available for Internet Explorer.

These days, most people are probably using transpilers like Babel somewhere in their build pipelines, but that's not true for everyone. BuckleScript does a great job of compiling mostly to the ES5 JS spec, but this is one case where it does not (it's specified in ES6), and it won't tell you about that ahead of time.

Just so people are aware...

Collapse
 
yawaramin profile image
Yawar Amin

Good point! I wasn't aware of that. Btw I think BuckleScript is looking to move forward with more ES6 features at some point–good to keep in mind for the future.