3 minute read

The official Scala.js docs are actually quite good. But the following cookbook contains specific use cases that you may come across quite often (that may be tangential to Scala.js itself and for which there may be little documentation) and some of the caveats you’ll find when declaring Scala.js types, that you’d otherwise find out about when the compiler yells at you

Defining JS types in Scala

docs.

You must have scalacOptions += "-P:scalajs:sjsDefinedByDefault" in your client project declaration in build.sbt (as @ScalaJSDefined annotations are now deprecated)

trait Foo extends js.Object {
  val bar: String
  val baz: Int
}

You can now cast js.Objects to your Scala.js type via jsObject.asInstanceOf[Foo]. Note unlike what you’re likely used to that happens when you call .asInstanceOf[Foo] i.e. an exception being throw immediately, here only when you reference a member i.e foo.bar or foo.baz will an exception be thrown

NOTE: Foo here isn’t a “facade”, but rather a “Scala.js-defined JS type” (as both are referred to) i.e. a declaration that will be compiled to a plain JS object. These are effectively JS types that whereby their members are visible from JS code, a constructor for them can be exported, and if this is done, JS classes can extend them.

To define a “facade”, that is to say an interface to some JS API. You must do:

@js.native
trait FooAPI extends js.Object {
  def bar(x: String) = js.native
}

Find more info about defining facades here

It’s also worth looking at how how Scala types map to Scala.js types: https://www.scala-js.org/doc/interoperabi lity/types.html , This is particularly important when defining facades. For example if you have an instance of a facade Bar with single member val foo: js.Function1[js.Function1[String, String], Int] as js.Dynamic.literal(foo = (f: Function1[String, String]) => 3).asInstanceOf[Bar] you’ll get a cryptic cast exception at runtime d ue to the fact that a scala Function1 is not a js.Function1

ADTs (Algebraic Data Types)

If you’d like Foo from above to be a member of some ADT, lets say Quux, the only way I could conceive of doing this, that most closely mirrors how you declare ADTs in Scala, is:

sealed trait Quux extends js.Any

// as above
trait Foo extends js.Object with Quux { ... }

However this likely will not be very useful to you. You can’t pattern match on a Foo for example as the compiler complains that it is a raw JS trait. Also case modifiers are not allowed on classes that extend js.Object. So your best bet oftentimes is to cast to js.Dynamic and access the fields you need to construct your ADT members precariously:

// Regular Scala ADT
sealed trait Quux
case class Foo(x: String, y: Int) extends Quux
case class Bar(z: String) extends Quux

object Quux {
  def fromJSObject(obj: js.Object): Option[Quux] = {
    val dyn = obj.asInstanceOf[js.Dynamic]
    dyn.blah.asInstanceOf[String] match {
      case "identifiesAFoo" =>
        Some(Foo(dyn.x.asInstanceOf[String], dyn.y.asInstanceOf[Int]))
      case "identifiesABar" =>
        Some(Bar(dyn.z.asInstanceOf[String]))
      case _ => None
    }
  }

}

Obviously this will only work if your ADT is shallow. Otherwise you’re going to just have to bite the bullet and declare your ADT as a scaljs defined type, rather than vanilla Scala. The reason the runtime representation of case classes isn’t, in the case of Foo above for example, var foo = {x: "hello", y: 5} taking a random instance but rather: `var foo = {x$1: “hello”, y$2: 5}. This is due to cross compilation consistency with the JVM.

Miscellaneous

Adding JS, CSS, IMG, etc static assets

In build.sbt:


lazy val client
  ...
  npmAssets ++= NpmAssets
    .ofProject(client) { nodeModules: sbt.File =>
      (nodeModules / "bootstrap" / "dist" / "css").allPaths +++
      (nodeModules / "react-select" / "dist").allPaths +++
      (nodeModules / "react-octicons-svg" / "dist").allPaths
    }
  .value,
  ...

Lets say you wanted to add bootstrap.js which lives under node_modules/bootstrap/dist/js/bootstrap.js You would add the following line: (nodeModules / "bootstrap").allPaths in place of (nodeModules / "bootstrap" / "dist" / "css").allPaths

And then dont forget to add::

<script type="text/javascript" src="/public/bootstrap/dist/js/bootstrap.js"></script> to src/main/public/index.html`

Otherwise the asset will not be returned when you do your initial GET on <host>:<port>/ after starting the server

Updated:

Comments