Typed façade for JS library in Scala.js

2019-01-26 18:52发布

I am trying to write a typed façade for my library Paths.js, following the official guide.

What I would like to be able to translate is a call like the following:

var Polygon = require('paths-js/Polygon');
var polygon = Polygon({
  points: [[1, 3], [2, 5], [3, 4], [2, 0]],
  closed: true
});

into

val polygon = Polygon(points = List((1, 3), (2, 5), (5, 6)), closed = true)

but I am not sure what I need to do to get to this point.

What I have done is something like the following

type Point = (Number, Number)
trait PolygonOpt {
  val points: Array[Point]
  val closed: Boolean
}
@JSName("paths.Polygon")
object Polygon extends js.Object {
  def apply(options: PolygonOpt): Shape = js.native
}

Then, I can invoke it like

class Opt extends PolygonOpt {
  val points: Array[Point] = Array((1, 2), (3, 4), (5, 6))
  val closed = true
}
val opts = new Opt
val poly = Polygon(opts) 

I have a few doubts on this:

  • I am at a point where everything compiles, but the resulting javascript fails at the point of invocation. I believe that this is because I am passing an instance of PolygonOpt, while the runtime expects a javascript object literal
  • is the definition of Point translated into a js array with two components?
  • I would like to be able to overload Polygon.apply like def apply(points: Seq[Point], closed: Boolean): Shape, but scala.js does not let me write method implementation inside Polygon since it extends js.Object

Moreover, I have both a version of my library using common.js (which is split into several files, one for each component) and another one which can be used as a single <script> tag, putting everything under the namespace paths (which is the one I am using right now).

Which one works better for a Scala.js wrapper?

1条回答
兄弟一词,经得起流年.
2楼-- · 2019-01-26 19:31

First, make sure to read the JS interop doc, including the calling JavaScript guide. I think you did so already, because you already have something reasonable. But you should pay particular attention to the parts that say that Scala types and JavaScript types are completely unrelated unless explicitly mentioned.

So, an Int is a proper JS number (in the range of an int). But an Array[Point] has nothing to do with a JavaScript array. A Tuple2 (such as (1, 3)) even less so. So:

Is the definition of Point translated into a js array with two components?

No, it is not. As such, it is completely incomprehensible to JavaScript. It is opaque.

Worse, a PolygonOpt, since it does not extend js.Object, is completely opaque as well from JavaScript, which explains why you cannot see the fields points and closed.

The first thing to do is to type accurately, with JavaScript-understandable types (extending js.Object), your JS API. In this case, it would look like this:

type JSPoint = js.Array[Int] // or Double

trait PolygonOpts extends js.Object {
  val points: js.Array[JSPoint] = js.native
  val closed: Boolean = js.native
}

@JSName("paths.Polygon")
object Polygon extends js.Object {
  def apply(options: PolygonOpt): Shape = js.native
}

Now, the thing is that it is not really easy to create an instance of PolygonOpts. For this refer to this SO question:

object PolygonOpts {
  def apply(points: js.Array[JSPoint], closed: Boolean): PolygonOpts = {
    js.Dynamic.literal(
        points = points,
        closed = closed
    ).asInstanceOf[PolygonOpts]
  }
}

Finally, you can expose the Scala-esque API you wanted in the first place with implicit extensions:

import js.JSConverters._

object PolygonImplicits {
  implicit class PolygonObjOps(val self: Polygon.type) extends AnyVal {
    def apply(points: List[(Int, Int)], closed: Boolean): Shape = {
      val jsPoints =
        for ((x, y) <- points.toJSArray)
          yield js.Array(x, y)
      Polygon(PolygonOpts(jsPoints, closed))
    }
  }
}

Implicit extensions are the way to write Scala methods that appear to be available on objects extending js.Object, since, as you discovered, you cannot actually implement methods inside a js.Object.

查看更多
登录 后发表回答