Cannot able create a Record from another Record (With all optional keys)


type a = {
  t ?: string,
  l ?: int,
  p ?: array<int>
}

type b = {
  t ?: string,
  l ?: string,
  p ?: string
}

let intToString : option<int> => option<string> = (s) => {
	switch s{
  	 | Some(r) => Some("int")
     | None => None 
  }
}
let arrayToString : option<array<int>> => option<string> = (s) => {
	switch s{
  	 | Some(r) => Some("array")
     | None => None 
  }
}

let mapper: (a) => b = (a) => {
  ({ t : ?a.t
  , l : ?a.l->intToString
  , p : ?a.p->arrayToString
  } : b)
}

let s = mapper({t : "Testing" })
Above line is giving this output { t : "Testing", l : undefined, p : undefined }
but I wanted to get this output { t : "Testing" }

How to write code for this in rescript ?

As you can see in the playground, the generated JS code for the mapper function is the following:

function mapper(a) {
  return {
          t: a.t,
          l: intToString(a.l),
          p: arrayToString(a.p)
        };
}

What JS code would you write to achieve what you’re looking for?

By the way, what is the problem with having present and undefined fields instead of absent fields?

I was thinking of writing this code in Js to get my use case.

function mapper(a) {
  const result = {};
  if (typeof a.t !== "undefined") result.t = a.t;
  if (typeof a.l !== "undefined") result.l = intToString(a.l);
  if (typeof a.p !== "undefined") result.p = arrayToString(a.p);
  return result;
}

So, I tried writing this in Rescript, But failed because of compilation errors .

Also, if rescript’s com gives a feature where we asign optional to a optional field and code gets generated like this. Then, we can get type safety with this feature

Ex :

let mapper: (a) => b = (a) => {
  ({ t ?: a.t
  , l ?: a.l->intToString
  , p ?: a.p->arrayToString
  } : b)
}

The issue is mostly performance. Where I have to write a function to loop over object and remove keys that have undefined value.

What is the original problem you’re trying to solve?

1 Like

I can hypothesize that the problem could be related to some kind of TS type or some strict runtime check. In other words it could be something like the following:

interface B {
  t?: string;
  l?: string;
  p?: string;
}

is different from

interface B {
  t?: string | undefined;
  l?: string | undefined;
  p?: string | undefined;
}

Or, related to runtime check, t in a gives different results than a.t !== undefined for those cases.

Yes, because if null is a one million dollar mistake, JS had the nice idea to have 3 different ways of expressing a slightly different variant of a null value :sweat: (undefined, null and the absence of the field).


I would like to get an answer from the OP as well, but at the same time I am curious about possible approaches in Rescript (other than going %raw). Said that, there are a lot of JS projects out there that don’t even distinguish a null from a 0 or an empty string, so…

EDIT: just to throw ideas, this should work, but obviously it requires a bit of boilerplate.

1 Like

You would need the same boilerplate in js too anyway, right? I don’t think you can really make something simpler here.

Partially yes. Sorry, I have not been specific with my words – the boilerplate I am referring to is the need of a bMutable type to being able to modify the record. But anyway, I think that this problem only exists because of some terrible design decisions in JS, I am open to ideas but I am not sure Rescript should actually have some additional features to handle this case in a more ergonomic way.

1 Like

Apologies for missing your reply earlier.

I’m creating a JSX using the generic JSX feature of ReScript. We have a native cross-platform renderer that requires a prop in string format. However, I don’t want developers to write margins in string format like “20,20,20,20”. To address this, I’ve defined two prop types:

type margin = Margin(int, int, int, int)
type props = {key?: string, margin?: margin}

type renderProps = {key?: string, margin?: string}

Now, I need a mapper function to convert between these types. However, I encountered an issue where all keys were converting to undefined. My native renderer checks for the presence of props using hasOwnProperty and other contracts. I don’t want to change my native renderer, and I also dislike the idea of writing a JavaScript loop to remove all keys with undefined values.

I’m unable to think of any solution from the ReScript side either…

It’s not pretty, but the boilerplate can be written in rescript, via the object spread operator. Working off of the initial example:

type a = {
  t ?: string,
  l ?: int,
  p ?: array<int>
}

type b = {
  t ?: string,
  l ?: string,
  p ?: string
}

let updateT : (b, a) => b = (b, a) => {
	switch a.t {
  	 | Some(r) => {...b, t: r}
     | None => b
  }
}
let updateL : (b, a) => b = (b, a) => {
	switch a.l {
  	 | Some(r) => {...b, l: "int"}
     | None => b 
  }
}
let updateP : (b, a) => b = (b, a) => {
	switch a.p {
  	 | Some(r) => {...b, p: "array"}
     | None => b
  }
}

let mapper: (a) => b = (a) => 
  {}
  ->updateT(a)
  ->updateL(a)
  ->updateP(a)

Alternatively, since there aren’t that many fields here, all of the pattern matching could be done at once.

let mapper: (a) => b = (a) => 
  switch a {
    | {t, l, p} => {t, l: "int", p: "array"}
    | {t, l} => {t, l: "int"}
    | {t, p} => {t, p: "array"}
    | {l, p} => {l: "int", p: "array"}
    | {t} => {t: t}
    | {l} => {l: "int"}
    | {p} => {p: "array"}
    | {} => {}
  }

Not that I’d recommend this for larger records.

I have 250 fields in my prop type.

For now, I did this

let mapper: (a) => b = (a) => {
  ({ t ?: a.t
  , l ?: a.l->intToString
  , p ?: a.p->arrayToString
  } : b)
}

But in this also, If you remove any key for code … rescript will compile successfully. There is no way to ensure all keys are copied. That needs to be manage by team.