How to bind a JS function that has multiple and recursive return types

In github:jorgebucaran/hyperapp (can’t link because of max-2-links-per-post limitation), which also uses a virtualdom, the view is essentially a composed form of h() and text().

h : (tag: String, props: Object, children: VNode? | [...VNodes]?) -> VNode

eg.,

h("label", {}, [
	text("First name"),
	h("input", {type: "text", value: "hello"}, [])
  ])

I am trying to type this h() function. I have managed to make the basic stuff work. Playground link:

type vnode

@module("@hyperapp/html") @val external text: string => vnode = "text"

// props is essentially a plain object representing a set of html attributes.
type props
@obj external props: (~\"type": string=?, ~value: string=?, unit) => props = ""

@module("hyperapp") @val
external h: (string, props, array<vnode>) => vnode = "h"

let input = h(
  "label",
  props(),
  [text("First name"), h("input", props(~\"type"="text", ()), [])],
)

I am stuck at trying to type the event handlers (onchange, etc.) properties in props.

eg., h("input", {onchange: [actionFn, payload], []}), where

[actionFn, payload] is an action descriptor;

actionFn is

actionFn : (state, payload) -> | [state, ...effects] // returns a new state, with zero or more effects to be executed, OR
                               | [actionFn, payload] // returns an action descriptor to dispatch another action

Thus, the actionFn returns either of the two tuples.

I have experimented a bit, but pasting those erroneous pieces of code here will make the post unecessarily long. Instead, let me link the playground.

So, how do I bind the h() function, or rather how do I type props ? Any help is appreciated.

Not sure this is the best way to do this, but something I would try: playground

type state
type payload

type dispatch
type effecter = (dispatch, payload) => unit
type effect = (effecter, payload)

module ActionResult = {
  type t
  external justState: state => t = "%identity"
  external withEffect: ((state, effect)) => t = "%identity"
  external withEffects2: ((state, effect, effect)) => t = "%identity"
  external withEffects3: ((state, effect, effect, effect)) => t = "%identity"
}

type action = (state, payload) => ActionResult.t
type actionDescriptor = (action, payload)

// attempt1
@deriving(abstract)
type props = {
  onchange: actionDescriptor,
}
1 Like

While the official docs files your %identity suggestion under Dangerous type case, I haven’t understood the interop layer enough to find any other alternative by myself.

On the other hand, I built on your suggestion successfully. Playground link

Let me wait for a couple of (working) days to see if anyone else has another solution. Then I will mark your answer as the solution.

1 Like

Posting the initial working iteration of the code I built on @rpominov’s suggestion, for posterity:

type state = {name: string, printNum: int}

type actionDescriptor
type effectDescriptor
type dispatch = (. actionDescriptor) => unit

module ActionDescriptor = {
  external toState: (state => state) => actionDescriptor = "%identity"
  external toStateWithEffect: (
    state => (state, effectDescriptor)
  ) => actionDescriptor = "%identity"
  external toStateWithEffects2: (
    state => (state, effectDescriptor, effectDescriptor)
  ) => actionDescriptor = "%identity"
  external toStateWithEffects3: (
    state => (state, effectDescriptor, effectDescriptor, effectDescriptor)
  ) => actionDescriptor = "%identity"
  external toActionDescriptor: (state => actionDescriptor) => actionDescriptor =
    "%identity"

  external usingEventToState: ((state, 'ev) => state) => actionDescriptor =
    "%identity"
  external usingEventToStateWithEffect: (
    (state, 'ev) => (state, effectDescriptor)
  ) => actionDescriptor = "%identity"
  external usingEventToStateWithEffects2: (
    (state, 'ev) => (state, effectDescriptor, effectDescriptor)
  ) => actionDescriptor = "%identity"
  external usingEventToStateWithEffects3: (
    (
      state,
      'ev,
    ) => (state, effectDescriptor, effectDescriptor, effectDescriptor)
  ) => actionDescriptor = "%identity"
  external usingEventToActionDescriptor: (
    (state, 'ev) => actionDescriptor
  ) => actionDescriptor = "%identity"

  external usingPayloadToState: (
    ((state, 'p) => state, 'p)
  ) => actionDescriptor = "%identity"
  external usingPayloadToStateWithEffect: (
    ((state, 'p) => (state, effectDescriptor), 'p)
  ) => actionDescriptor = "%identity"
  external usingPayloadToStateWithEffects2: (
    ((state, 'p) => (state, effectDescriptor, effectDescriptor), 'p)
  ) => actionDescriptor = "%identity"
  external usingPayloadToStateWithEffects3: (
    (
      (
        state,
        'p,
      ) => (state, effectDescriptor, effectDescriptor, effectDescriptor),
      'p,
    )
  ) => actionDescriptor = "%identity"
  external usingPayloadToActionDescriptor: (
    ((state, 'p) => actionDescriptor, 'p)
  ) => actionDescriptor = "%identity"
}

module EffectDescriptor = {
  external justEffect: (dispatch => unit) => effectDescriptor = "%identity"
  external effectWithPayload: (
    ((dispatch, 'p) => unit, 'p)
  ) => effectDescriptor = "%identity"
}

type props
@obj external props: (~onchange: actionDescriptor=?, unit) => props = ""

let updateName = (state, name) => {
  {...state, name: name}
}
let p = props(
  ~onchange=ActionDescriptor.usingPayloadToState((updateName, "Jayesh")),
  (),
)

let namePrinted = state => {
  {...state, printNum: state.printNum + 1}
}

let print = (dispatch, msg) => {
  Js.log(msg)
  dispatch(. ActionDescriptor.toState(namePrinted))
  ()
}
let updateNameWithPrint = (state, name) => {
  ({...state, name: name}, EffectDescriptor.effectWithPayload((print, name)))
}
let p2 = props(
  ~onchange=ActionDescriptor.usingPayloadToStateWithEffect((
    updateNameWithPrint,
    "Jayesh",
  )),
  (),
)