Function returning JS objects of different shapes


#1

Hi,

I’m in the middle of migrating to ReasonML and I have to work with JS objects.
I’m trying to create a function that returns different objects depending on the input.
Here’s a simplified example:

type something = | A | B;

let doSomething = s => switch s {
  | A => {"a": true}
  | B => {"b": true}
}

This fails because of the different object shapes.
Is there a way to do something like this?

Thanks!


#2

Your simplified example opens a loop hole:

type something =
  | A
  | B;

[@bs.deriving abstract]
type someJs = {
  a: Js.undefined(bool),
  b: Js.undefined(bool)
};

let makeJsA = (v) => someJs(~a=Js.Undefined.return(v), ~b=Js.Undefined.empty);

let makeJsB = (v) => someJs(~b=Js.Undefined.return(v), ~a=Js.Undefined.empty);

let doSomething = (s) =>
  switch s {
  | A => makeJsA(true)
  | B => makeJsB(true)
  };

So on this side it is the same type (someJs).

PS: scratch that:

> let f = () => ({a: true, b: undefined});
undefined
> let g = () => ({a: undefined, b: true});
undefined
> let objA = f();
undefined
> let objB = g();
undefined
> Object.getOwnPropertyNames(objA);
(2) ["a", "b"]
> Object.getOwnPropertyNames(objB);
(2) ["a", "b"]

When I used Js.log(doSomething(B)) in https://reasonml.github.io/en/try the console gave me {"b":true}.

type something =
  | A
  | B;

let makeJs = (k, v) => Js.Dict.fromList([(k, v)]);

let doSomething = (s) =>
  switch s {
  | A => makeJs("a", true)
  | B => makeJs("b", true)
  };

#3

Thanks, my issue is that I have a lot of object shapes I need to map to (>50) and I’m trying to do that without declaring each shape.
Since this code is bridging between reason and JS, type safety is as good as my bindings are, so I’m not sure there’s a difference between creating 50 object types manually or just returning whatever shape I want from that function.
Unless I’m completely wrong :slight_smile:


#4

Static typing is about correctness - not convenience. The compiler is supposed so ensure that all “contracts” are being adhered to and while type inference can help reduce the noise, the “contracts” are usually laid out as types.

To that end Js.Dict models a JS object as a hash map while [@bs.deriving abstract] models it as a record.

That being said there may be some sort of escape hatch … but Bucklescript/Reasonml values sound typing - something that is a non-goal for many other (so called) statically typed languages.

How about:

type something =
  | A
  | B;

let makeObj = (obj) => Js.Obj.assign(Js.Obj.empty(), obj);

let doSomething = (s) =>
  switch s {
  | A => makeObj({"a": true, "c": "A"})
  | B => makeObj({"b": true, "d": "B"})
  };

let () = Js.log(doSomething(A));

#5

The escape hatch seems to be:

type something =
  | A
  | B;

type jsObj;

type jsA = {. "a": bool};

type jsB = {. "b": bool};

external castJsA : jsA => jsObj = "%identity";

external castJsB : jsB => jsObj = "%identity";

let doSomething = (s) =>
  switch s {
  | A => castJsA([%obj {a: true}])
  | B => castJsB([%obj {b: true}])
  };

let () = Js.log(doSomething(A));

#6

Thanks @peerreynders, I really appreciate your time and all the information you provided, there’s a lot of good stuff here.
I totally agree that the right thing to do is to type all the objects, I guess I was too lazy :slight_smile:


#7

Less verbose use of the escape hatch:

type something =
  | A
  | B;

external castToJsObj : Js.t({..}) => Js.t({..}) = "%identity";

let doSomething = (s) =>
  switch s {
  | A => castToJsObj({"a": true})
  | B => castToJsObj({"b": true})
  };

Js.log(doSomething(B));

#8

Yes, makes perfect sense.
Thanks again!