Rescript Equivalent of Normalizing Javascript Object

Im trying to understand what the rescript equivalent of something like this is:

type UseStylesOptionsType = {
  normalize?: boolean;
  darkmode?: boolean;
}
let options: UseStylesOptionsType = {
  normalize: true,
  darkmode: true,
};

if (options.normalize) {
    const excludeProperties = ['flex'];
    for (const key in themedStyles) {
      if (Object.prototype.hasOwnProperty.call(themedStyles, key)) {
        for (const key2 in themedStyles[key]) {
          if (Object.prototype.hasOwnProperty.call(themedStyles[key], key2)) {
            const value = themedStyles[key][key2];
            if (typeof value === 'number' && !excludeProperties.includes(key2)) {
              themedStyles = {
                ...themedStyles,
                [key]: {
                  ...themedStyles[key],
                  [key2]: normalize(value),
                }
              }
            }
          }
        }
      }
    }
  }

Not really clear at what the concepts are so please feel free to be didactic. Thank you.

2 Likes

It looks like the code is taking an dictionary-style object with different types stored inside it. It’s not clear what normalize does, but I assume it’s applying some kind of transformation that will be seen in the return object.

I don’t think there is a way to do this with pure ReScript in a 1-to-1 way. If the data was structured differently, then you could do something like this.

E.g., if you have a Js.Dict.t that contains a variant type that covers all the different cases for each data type you care about (in this case, number/float and whatever else), then you could write similar code for that data structure.

Although I suppose you could do something closer to the original code with the @unboxed existential GADT trick, using the Js.typeof function to type-check the values at runtime.

But in general, this kind of dynamic object manipulation is very cumbersome in ReScript. If a library exposes its own functions to do things like this, then it’s often good to just let it do its thing, and try to write type-safe bindings where possible. But I probably wouldn’t try to write this function in pure ReScript if I have control over the data structures involved.

2 Likes

Yes, this can be done with Js.Dict and some unboxed variants, yes, this is probably not the ReScript way, and yes, it might still make sense if you’re interacting with some existing JS implementation.

But if you could model it from scratch: if it’s CSS rules, you’d probably want to model it as a record (or maybe as a list, like bs-css currently does), not as a polymorphic dictionary, because the latter doesn’t give you much type safety.

And speaking of type safety, if you make your normalize function return something like Normalized(float), it’ll make it impossible to use non-normalized data where normalized is required. Whatever normalization is (still curious).

P.S. Btw, the TS code could be simplified if you used Object.keys(themedStyles).forEach(...) (Object.keys only lists names of own properties), and also just go themedStyles[key][key2] = normalize(value) (you could copy themedStyles once, but why copy it in a nested loop? It’s bad for both readability and performance).

4 Likes

WARNING: Just realized this answer got very long and possibly even confusing. I’m putting this to indicate you to take all Im saying with a little caution, as I’m also still learning and maybe wrong. It describes how i went through the problem of porting code dealing with Js-Objects with dyn/computed keys, which I think is the main problem hidden behind the question and what my opinion is. I just hope you can still extract value out of this…

@hoichi
I think the normalize function just makes sure the value is converted to a string (e.g. 1 --> “1px”) and is not what Normalizing from the title actually refers to, which is just the code example as whole i suppose. Roughly:
“Having a js-object & iterating over its props/keys & doing stuff”

I had a similar question a long while ago. What normalizing means in rescript is to have data of type A and convert it to data of type B via a functions which takes an arg of type A and returns data of type B obiously or option(B) when can fail.
So to convert your data themedStyles in a type-safe way you’d have to be explicit about the “schema”, e.g. define the type of your data.

// suppose themedStyles looks like:
 const themedStyles = {
   lightStyle:  {
     flex: 0,
     width: "15px",
   },
   darkStyle:  {
     flex: 1,
     width: "5px",
   },
 }

Since your JS-Code is ultra-dynamic, i can only assume the shape of your objects. Your types might look like:


// following is the type of what is inside themedStyles[key]
// because nesting record type definitions is not possible
type themedStyleType = {
  flex: int,
  width: string,
}
// following is the type of your data themedStyles
type allThemedStyles = {
  lightStyle: themedStyleType,
  darkStyle: themedStyleType,
}

Note that i made up names for the keys,e.g. lightStyle, which are dynamic in your code-example. Sometimes the value of the key is coming from user-input and you don’t have any chance to type like above. If you really have no idea how your object looks like, you can’t define it this way resulting in less type-safety.
In such cases you could define it like @austindd suggested:

type allThemedStyles = Js.Dict.t(themedStyleType)

This is kinda equivalent to js-objects. But with Js.Dict as you can see, you declare that you can have arbitrary string-values for your key, but the value part has to be of type themedStyleType.

From Rescript side you can not pass anything else. When JS does, it can crash when the defined property like width has a wrong type at runtime and will crash definetly when the prop doesn’t exist at all.
Extra properties which are not defined do not cause runtime crashes. But i had issues where these were just dropped, so i wouldn’t rely on these “hidden” props ^^

But sometimes there is an implicit schema behind your objects. Let’s say you have maybe also a hight-property. Unlike darkStyle which in our example is custom, this is precise enough to define a type for:

type allThemedStyles = {
  flex: int,
  width: string,
  height: Js.Nullable.t(string)
}

With Js.Nullable.t(string) you declare that height could also be null or undefined.
What i ended up doing at work was putting this nullable-thing almost everywhere, cause you never know for sure :grin: And when it crashes at runtime it was super-hard to debug. If you can’t trust your data at all your types would be like:

type themedStyleType = {
  flex: Js.Nullable.t(int),
  width: Js.Nullable.t(string),
  height: Js.Nullable.t(string)
}

type allThemedStyles = Js.Dict.t(Js.Nullable.t(themedStyleType));

which is super verbose and annoying, since you also would have to convert these to an option type at some point. And it is not even complete, cause flex maybe be a string…

Well, i didn’t find a satisfying answer to this problem. You could use the typed defined in bs-css which @hoichi mentioned if your types match the css-specs. Not sure, but maybe bs-css also has decodiung/encoding functions already. Also there are lib like bs-json for conversion.

Honestly, i think it is not worth it in these cases to mimic existing js-code.
All these types i described above are the attempt to model a single computation step we did somewhere in the js-world. Interop and reusing js-functions is great, but each back & forth requires specifying types which probably would never exist otherwise.
Plus: One is kinda still stuck in Thinking-Js-Way instead of leveraging variants and other cool stuff. These nullable types should only exist once to model data entering the strict rescript-world from outside like server-responses and than mapped directly to proper types, like Option or even Result where you get forced to think about what error cases you actually have in your code. This process makes one a better programmer, but takes time…
If you just want it to work, you can still use raw js.

My Conclusion is begin converting with parts where you know your data exactly (rules out all places where you do { ...spread } and dont really know whats inside). write the types easily and little functions working with these. Otherwise you’ll get frustrated, cause in the back of your mind you know: in js you can just spread that sh** :rofl:

Sorry, i know that’s not satisfying at all…
Please correct, if i wrote anything wrong, I’d love to be proved wrong.

2 Likes

Never said thank you. So thank you @hoichi @austindd @mashalla

2 Likes