Skip to content

Latest commit

 

History

History
149 lines (116 loc) · 4.89 KB

File metadata and controls

149 lines (116 loc) · 4.89 KB
title API Modelling
description Learn more about the API modelling process of @rescript/webapi.
slug 05-api-modelling

import { Aside, Code, Icon } from "@astrojs/starlight/components";

One of this projects goals is to provide a consistent and idiomatic API for the Web APIs. The interopt story of ReScript is quite good, but it it has limitations. JavaScript is a dynamic language and has a lot of flexibility. ReScript is a statically typed language and has to model the dynamic parts of JavaScript in a static way.

Dynamic parameters and properties

Some Web APIs have a parameter or property that can be multiple things. In ReScript, you would model this as a variant type. This is an example wrapper around values with a key property to discriminate them.

In JavaScript, this strictness is not enforced and you can pass a string where a number is expected. There are multiple strategies to model this in ReScript and it depends on the specific API which one is the best.

Overloads

One example is addEventListener.
This can access either a boolean or an object as the third parameter.

addEventListener(type, listener);
addEventListener(type, listener, options);
addEventListener(type, listener, useCapture);

Because, this is a method, we can model this as an overloaded function in ReScript.
The first two overloads are the same, so we can merge them into one with an optional options parameter.

@send
external addEventListener: (
  htmlButtonElement,
  eventType,
  eventListener<'event>,
  ~options: addEventListenerOptions=?,
) => unit = "addEventListener"

The third overload takes a boolean and is worth using when you want to change the default of the useCapture boolean parameter.
We can use a fixed argument to model this.

@send
external addEventListener_useCapture: (
  htmlButtonElement,
  ~type_: eventType,
  ~callback: eventListener<'event>,
  @as(json`true`) _,
) => unit = "addEventListener"
Be aware that inherited interfaces duplicate their inherited methods. This means that improving `addEventListener` in the `EventTarget` interface requires to update all interfaces that inherit from `EventTarget`.

Decoded variants

We can be pragmatic with overloaded functions and use model them in various creative ways.
For properties, we cannot do this unfortunately. A propery can only be defined once and have a single type.

The strategy here is to use a decoded variant.

Example for the fillStyle property of the CanvasRenderingContext2D interface can be either a:

  • string
  • CanvasGradient
  • CanvasPattern

These types are not all primitives and thus we cannot define it as untagged variants.
What we can do instead is represent the type as an empty type and use a helper module to interact with this.

export const fillStyleDef = ` type fillStyle

type canvasRenderingContext2D = { // ... other propeties mutable fillStyle: fillStyle } `;

When we wish to read and write the fillStyle property, we can use a helper module to lift the type to an actual ReScript variant:

export const fillStyleModule = ` open Prelude open CanvasAPI open DOMAPI

external fromString: string => fillStyle = "%identity" external fromCanvasGradient: canvasGradient => fillStyle = "%identity" external fromCanvasPattern: canvasGradient => fillStyle = "%identity"

type decoded = | String(string) | CanvasGradient(canvasGradient) | CanvasPattern(canvasPattern)

let decode = (t: fillStyle): decoded => { if CanvasGradient.isInstanceOf(t) { CanvasGradient(unsafeConversation(t)) } else if CanvasPattern.isInstanceOf(t) { CanvasPattern(unsafeConversation(t)) } else { String(unsafeConversation(t)) } } `

<Code code={fillStyleModule} title="DOMAPI/FillStyle.res" lang="ReScript"

We can now use FillStyle.decode to get the actual value of the fillStyle property.
And use FillStyle.fromString, FillStyle.fromCanvasGradient, and FillStyle.fromCanvasPattern to set the value.

let ctx = myCanvas->HTMLCanvasElement.getContext_2D

// Write
ctx.fillStyle = FillStyle.fromString("red")

// Read
switch ctx.fillStyle->FillStyle.decode {
| FillStyle.String(color) => Console.log(`Color: ${color}`)
| FillStyle.CanvasGradient(_) => Console.log("CanvasGradient")
| FillStyle.CanvasPattern(_) => Console.log("CanvasPattern")
}

Try and use decoded and decode as conventions for the type and function names.