Variant unwraping

Hello.

I’m trying to write bindings for ags ags docs

type baseProps = {
  name: string
}

module Label = {
  type t
  type props = {
   	...baseProps,
	label?: string,
  }
  
  @val @scope("Widget") external make: props => t = "Label"
}

module Box = {
  type t
  type widget = | Label(Label.t) | Box(t) // and more
  
  type props = {
   	...baseProps, // and additional optional fields
    child?: widget,
	children?: array<widget>,
  }
  
  @val @scope("Widget") external make: props => t = "Box"
}

I have to unwrap child and children somehow. If I’m right, I can use @unwrap this way

type baseProps = {
  "name": string
}

module Label = {
  type t = {
    widgetType: string,
  }
  
  type props = {
   	...baseProps,
	label?: string,
  }
  
  @val @scope("Widget") external make: props => t = "Label"
}

module Box = {
  type t
 
  @val @scope("Widget") external make: {
	...baseProps,
    "child": @unwrap [#Label(Label.t) | #Box(t) ],
	"children": array<@unwrap [#Label(Label.t) | #Box(t) ]>,
 } => t = "Box"
}

But in this case I can’t make fields optional. How can I solve it?
Perhaps I can write something like

module Box = {
  type t
 
  @val @scope("Widget") external make: {
	"name": option<string>,
    "child": option<@unwrap [#Label(Label.t) | #Box(t) ]>,
	"children": option<array<@unwrap [#Label(Label.t) | #Box(t) ]>>,
 } => t = "Box"
}

but it will be annoying

Hi L33! I’ll try to answer you though I have zero knowledge of AGS.

Be careful, you’re mixing record and object type declarations, you probably want to define records here since, they’re overall easier to use and provide better error messages (and that’s what JSX components expect though I’m not sure you’re using JSX here).

What do you mean by unwrapping? I guess you mean you want them to be unboxed/untagged right? This is not possible if they’re both objects or of unknown/opaque types. You can check the docs about untagged variants if you want to know more about them.

If you’re only producing those objects that are then consumed on the JS side, you can fix it in two different ways:

  1. the easiest solution is just to define different bindings for each type of child
module Box = {
  type t

  module WithLabelChildren = {
    type props = {
      ...baseProps,
      child: Label.t,
      children: array<Label.t>,
    }
    @val @scope("Widget") external make: props => t = "Box"
  }
  module WithBoxChildren = {
    type props = {
      ...baseProps,
      child: t,
      children: array<t>,
    }
    @val @scope("Widget") external make: props => t = "Box"
  }
}

The problem here is that you’ll probably need to do that for more than two types so it can’t get cumbersome and you can’t have an array of mixed types.

In order to circumvent those issues you can use an opaque type instead (playground link):

type widget

type baseProps = {name: string}

module Label = {
  type t = {widgetType: string}
  external toWidget: t => widget = "%identity"

  type props = {
    ...baseProps,
    label?: string,
  }

  @val @scope("Widget") external make: props => t = "Label"
}

module Box = {
  type t
  external toWidget: t => widget = "%identity"

  type props = {
    ...baseProps,
    child: widget,
    children: array<widget>,
  }
  @val @scope("Widget") external make: props => t = "Box"
}

If you want to avoid having to explicitly cast to widget every time, you can also a phantom type parameter for widget, but to be honest that opens another can of worms in terms of typing, not sure it’s actually worth the tradeoff (playground link):

type widget<'kind>

type baseProps = {name: string}

module Label = {
  @get external widgetType: widget<[> #Label]> => string = "widgetType"

  type props = {
    ...baseProps,
    label?: string,
  }

  @val @scope("Widget") external make: props => widget<[> #Label]> = "Label"
}

module Box = {
  @get external boxType: widget<[> #Box]> => string = "boxType"
  type props = {
    ...baseProps,
    child?: widget<[#Box | #Label]>,
    children?: array<widget<[#Box | #Label]>>,
  }
  @val @scope("Widget") external make: props => widget<[> #Box]> = "Box"
}

let labelWidget = Label.make({name: "label"})
let widgetType = Label.widgetType(labelWidget)
let boxWidget = Box.make({
  name: "box",
  children: [labelWidget],
})
let boxWidget2Type = Box.make({
  name: "box2",
  children: [labelWidget, boxWidget],
})->Box.boxType
2 Likes

Thank you for your answer. It solves my problem.