[@bs.deriving abstract] vs [@bs.obj] external


#1

Other than wordiness, what’s the difference between

[@bs.obj] external makeProps: (~foo: string, ~bar: int=?, unit) => _ = "";

and

[@bs.deriving abstract]
type props = {
  foo: string,
  [@bs.optional]
  bar: int,
};

They seem to be effectively the same, but maybe they’re conceptually different.

And what’s the semantics of makeProps being called “external”. Is it because it generates a JS object, which is outside ML-land?


#2

When you create a code like this:

[@bs.obj] external makeProps: (~foo: string, ~bar: int=?, unit) => _ = "";

let make = makeProps(~foo="Test", ~bar=1, ());

You’ll get this output:

var make = {
  foo: "Test",
  bar: 1
};

When you write this:

[@bs.deriving abstract]
type props = {
  foo: string,
  [@bs.optional] bar: int
};

// bs.deriving abstract gives you a creation function
let make = props(~foo="Test", ~bar=1, ());

You’ll get:

var make = {
  foo: "Test",
  bar: 1
};

Basically the same. So, when to use one and other? This is my humble opinion

Use [@bs.obj] external makeProps... when you need to bind something to JS, transform props, etc.

This other approach [@bs.deriving abstract]... you can use to map something from JS to Reason in a Record Mode BS docs here.

Reproducing BS docs:

If your JS object:

  • has a know, fixed set of fields
  • might or might not contain values of different types

Record Mode gives you the ability do iterate with object like this:

[%%raw {|var make = { foo: "Test", bar: 1}|}]; // Some external code on JS.

[@bs.deriving abstract]
type props = {
  foo: string,
  [@bs.optional] bar: int
};

[@bs.val] external make: props = "make";

let foo = make->fooGet; // Do something with foo.

Hope this help you!


#3

So, roughly speaking, [@bs.deriving abstract] is for reading from JS, and [@bs.obj] is for writing to JS. That makes sense, thanks!

I haven’t noticed [@bs.deriving abstract] generates accessors, by the way, but that’s an important point, I guess.

It’s a pity the docs show how to create records with [@bs.deriving abstract] but don’t describe [@bs.obj] + external at all (only the %bs.obj + DSL and the sugar). I think it muddles the idiomatic way to create JS objects somewhat. Maybe it should be made clearer in the docs (note to self when I have some time to contribute).


#4

I cannot agree to abstract being for reading only! I mostly generate objects with it, usually like so:

module Options = {
  [@bs.deriving abstract]
  type t = {
    some: string,
    [@bs.as "other"]
    otherVal: string,
    [@bs.optional]
    imOptional: int,
  };

  let make = t;
};

let jsObj = Options.make(~some="some", ~otherVal="xxx", ());

Js.log(jsObj);
Js.log(jsObj->Options.someGet);

#5

But if you have a lot of optional fields (say, you’re binding to a JS component that has a lot of props), imOptional: int=? seems more concise. Then again, if you rename fields as well, you just can’t do it with [@bs.ojb] external, it seems.


#6

There is a flag -dsource so you can see the generated code.
If the generated code is too scaring, you can dump the cmi file, bsc xx.cmi just see the generated signature, in general bs.deriving generates more utility functions