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};
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};
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));
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);
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)
I wish there was a way to limit
props
to a subset of theobj
fields in a generic way (trivial in TypeScript, by the way). Otherwise, you can easily pass"agw"
and spend some time debugging whyage
fails to update.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!
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.
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':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?If the properties themselves are optional, true! That throws another thorn into the copy constructor generator idea.
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...
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.