In the ReScript ecosystem today we manage event listeners in a type-unsafe way, e.g. https://github.com/rescript-lang/rescript-react/blob/c3017ec4bbce5847c3b2da8d2d536450a3e2fd6d/src/ReactEvent.resi#L182
@bs.get external target: t => {..} = "target"
This allows unsafely getting any property from the event, which may result in a runtime type error.
@send external addEventListener: (T.t, string, Dom.event => unit) => unit = ""
This gives us only the abstract type Dom.event
, forcing us to unsafely downcast it to the exact event type we need. A mistake here can also result in a runtime type error.
I am proposing a new design for events and listeners, that is completely type-safe. However, it requires additional event types. Here is a rough sketch:
module Event: {
open Dom
/** t = target type, e = event type */
type name<'t, 'e>
type mouse<'t, 'e> = mouseEvent_like<'e>
@send
external addEventListener: (
eventTarget_like<_> as 't,
name<'t, 'e>,
@uncurry 'e => unit,
) => unit = "addEventListener"
@inline("click") let click: name<element_like<_> as 't, mouse<'t, _>>
} = {
open Dom
type name<'t, 'e> = string
type mouse<'t, 'e> = mouseEvent_like<'e>
@send
external addEventListener: (
eventTarget_like<_> as 't,
name<'t, 'e>,
@uncurry 'e => unit,
) => unit = "addEventListener"
@inline let click = "click"
}
// Test: change to Dom.node for type error
let add = (elem: Dom.element) => {
open Event
elem->addEventListener(click, Js.log)
}
The key here is the type given to the value click
. This abstract type is given type parameters which encode the type safety rules:
- The event can target only DOM elements i.e. only elements can handle this event
- It is specifically a mouse event
The other piece of the puzzle is the type given to addEventListener
. Its three parameters are given types that propagate the type safety rules from the event target, to the event handler function itself.
We need to introduce the name<_,_>
type here to enforce the safety rules, and also the type mouse<'t, 'e> = mouseEvent_like<'e>
and other event type aliases like that to capture the type of the event’s target.
To maintain the rules for all possible events, we need only maintain the list of abstract values that represent the names of the events, like click
. To circumvent the safety rules, we just need to use Obj.magic
or the %identity
external (of course, hopefully with very good reason).
I would appreciate any feedback on this.