Generic function types in an array?

Trying to define a type for an array of handler tuples

useEventHandlers([
  (document, "pointermove", (e: Dom.pointerEvent) => ()),
  (document, "pointerup", (e: Dom.pointerEvent) => ()),
  (document, "keyup", (e: Dom.keyboardEvent) => ()),
])

But I am running into an error about how the keyboardEvent does not match the pointerEvent of the other two.

What I’ve tried:

Adding a generic type type events<'a> = array<(Dom.element, string, 'a => unit)> but it’s the same issue. I’m thinking that’s because array has to be the generic means that each function uses that same generic.

Is there a way to have each handler function use a generic event type or a base event? Or am I destined for a wall given that arrays expect all the same types throughout?

1 Like

I suspect you will need to define a single type for all of your array elements. For example:

type event =
  | PointerMoveEvent(Dom.element, Dom.pointerEvent => unit)
  | PointerUpEvent(Dom.element, Dom.pointerEvent => unit)
  | KeyUpEvent(Dom.element, Dom.keyboardEvent => unit)

let events = [
  PointerMoveEvent(document, (_e: Dom.pointerEvent) => ()),
  PointerUpEvent(document, (_e: Dom.pointerEvent) => ()),
  KeyUpEvent(document, (_e: Dom.keyboardEvent) => ()),
]
1 Like

Thanks! That could work. Another option I found is

useEventHandlers([
  (document, "pointermove", (e: Dom.pointerEvent) => ()),
  (document, "pointerup", (e: Dom.pointerEvent) => ()),
])

useEventHandlers([
 (document, "keyup", (e: Dom.keyboardEvent) => ()),
])

I wonder if a hook that takes a single tuple would be best after all?

As I started to think through the use case, I don’t think that abstraction is good enough. It makes a lot of assumptions on how the underlying useEffect should work, and with arrays requiring the same type I felt less confident in this approach. Taking a step back, I found a more flexible solution:

module Event = {
  type eventListener<'a> = Dom.event_like<'a> => unit
  @send
  external addEventListener: (Dom.eventTarget_like<'a>, string, eventListener<'b>) => unit =
    "addEventListener"
  @send
  external removeEventListener: (Dom.eventTarget_like<'a>, string, eventListener<'b>) => unit =
    "removeEventListener"

  module Listener = {
    @ocaml.doc("
		* Creates an event listener for a target DOM element. Returns a cleanup
		* function to remove the listener
		*
		* @example
		let remove = DomUtils.Listener.make(document, \"onclick\", Js.Console.log)
		// later
		remove()
		")
    let make = (target, eventName, eventListener) => {
      target->addEventListener(eventName, eventListener)
      () => {
        target->removeEventListener(eventName, eventListener)
      }
    }
  }
}

That way it can be composed with a useEffect in any number of ways:

@react.component
let make = () => {
  React.useEffect(() => {
    let listeners = [
      DomUtils.Event.Listener.make(document, "pointermove", (e: Dom.pointerEvent) => ()),
      DomUtils.Event.Listener.make(document, "pointerup", (e: Dom.pointerEvent) => ()),
      DomUtils.Event.Listener.make(document, "keyup", (e: Dom.keyboardEvent) => ()),
    ]
    
   Some(() => listeners->Array.forEach(remove => remove()))
  })
  {React.string("Listening")}
}