Model Flexible JavaScript Objects?

Hey all, I have perhaps an interesting case where I’m writing a library that, at the base layer, should be very flexible in terms of what types are supported. The end user will almost never touch this base layer, they will use specialized APIs a few layers higher that formally express concrete type expectations. But, I’m struggling to express my “generic” layer in the first place.

To start, I have a very simple object which should look something like:

type t = {
  "type": string | NicelyTypedFunction,
  "props": Js.object,
}

Forgive my pseudo-syntax, I don’t yet know how to actually write this in Rescript. The part that I’m struggling with is that this “props” field on the object needs to be dynamic; at this layer in the codebase, any javascript value can come through that props object (not unlike React as it tries to marshal props to the DOM) and I don’t know how to express that detail in the formal Rescript type system.

Can anybody help me point in the right direction? Thanks

If “props” can have any js value, then how is it actually used? Can you provide more details on how the “props” is used?

Yep, ultimately “props” holds this collection of key/value pairs that will be passed on to an external/underlying “host” (here again, much like React and the DOM). The framework that I’m working on will sit atop several different “hosts” and each one may expect different sets of key/value props from the upper layer/framework. Ultimately it’s these hosts which determine what key/value props are expected and supported but there’s no way for the higher level framework to know what host it’s running against so it must remain totally generic from a type perspective

You can use abstract type for props. This way you do not define it’s structure in any way, but can cast anything you want to it.

type props // abstract type

type objectWithProps = {
  kind: string,
  props: props
}

type a = {
  id: string,
  count: int
}

external aAsProps : a => props = "%identity" // casting

let x = {
  kind: "a",
  props: aAsProps({id: "", count: 0})
}
3 Likes

Ah ok, wonderful!

I’ve explored your suggestion in the playground here

Am I correct that the expression

external asPropsType : 'a => props = "%identity" // casting

parameterizes this asPropsType function as taking a generic 'a and producing a props type? That is, generically maps arbitrary input to the props type?

It was quite helpful exploring rescript-react’s official bindings too: https://github.com/rescript-lang/rescript-react/blob/master/src/React.res

Another small question here– so I’m exploring this solution with genType and locally it seems like the following TSX file is produced from compiling the playground:

export abstract class props { protected opaque!: any }; /* simulate opaque types */

// tslint:disable-next-line:interface-over-type-literal
export type node = {
  readonly kind: string; 
  readonly props: props; 
  readonly children: node[]
};

export const createNode: (kind:string, props:props, children:node[]) => {
  readonly children: node[]; 
  readonly kind: string; 
  readonly props: props
} = function (Arg1: any, Arg2: any, Arg3: any) {
  const result = Curry._3(
/* WARNING: circular type node. Only shallow converter applied. */
  MyCompBS.createNode, Arg1, Arg2, Arg3);
  return result
};

export const seq: (_1:{ readonly hold?: boolean; readonly seq: number[] }, _2:node[]) => {
  readonly children: node[]; 
  readonly kind: string; 
  readonly props: props
} = function (Arg1: any, Arg2: any) {
  const result = Curry._3(
/* WARNING: circular type node. Only shallow converter applied. */
  MyCompBS.seq, Arg1.hold, Arg1.seq, Arg2);
  return result
};

Now, as far as I understand the currying, this is a great output. My only confusion is that it seems that the seq and createNode functions produce an output which has the same structure as the node type, why are those return types not just node?

Thanks!

Ah ok I’ve answered my own second question there– I believe in the playground link I shared I was returning an Object from createNode but the type definition of node was a Record. I’ve added quotes to the keys in the typedef of node and now everything seems to work neatly.

Just want to confirm my first question then:

Thanks!

your version of asPropsType would work too, but for me it’s too generic.

You said that there will be several “hosts” and each of them will have its own variant of props. Assuming props for every host will defined as a record, I suggest to have converter function for every record. Something like:

type genericProps

module HostA = {
  type props = { ... }

  external asGenericProps : props => genericProps = "%identity" 
}

module HostB = {
  type props = { ... }

  external asGenericProps : props => genericProps = "%identity" 
}
2 Likes

Got it, I appreciate your suggestion! Thank you :pray:

This might not be helpful in your use case but for something a little more type safe, you can capture the props type in the node type:
https://rescript-lang.org/try?code=AIcwpgdgKgngDmAUAF3mABAJzAY3RAewBMwAeAcgEMA+dAXnQG9F10BrASwiIC50BnZJi4gANC3RxMBOPz5VxrHAAsOAGyLYIfSpkyUYpQiQo1q4gL6JEoSLASI1YZOhzZKyMADliGBgApObj5BYQgxSWlZeUpRV1UNLR09AyNfU2pqAEp6WmZWRiDedi4iOKkZOUjKuJV1TUg+OsTIKysbcGg0R2cBMABHenR-CQA-ZQINPgAjAkm6ADNKNX4wRXRR1f7k-UMFtQIPcwlmhu10XV20k0YAIgmNW5m5tTjbraeLlL2Do4tM8Q5Oh5E7uTw+Ej+d4DW5xO4PIifBFvD4hAYWWoJM5ZRAWIA

The downside of this is it will be harder to hold a collection that contains multiple nodes with different props types, but an identity external can help with that.

1 Like