Export Scala.js APIs to JavaScript

By default, Scala.js classes, objects, methods and properties are not available to JavaScript. Entities that have to be accessed from JavaScript must be annotated explicitly as exported, using @JSExportTopLevel and @JSExport.

A simple example

package example

import scala.scalajs.js.annotation._

@JSExportTopLevel("HelloWorld")
object HelloWorld {
  @JSExport
  def sayHello(): Unit = {
    println("Hello world!")
  }
}

This allows to call the sayHello() method of HelloWorld like this in JavaScript:

HelloWorld.sayHello();

The @JSExportTopLevel on HelloWorld exports the object HelloWorld itself in the JavaScript global scope. It is however not sufficient to allow JavaScript to call methods of HelloWorld. This is why we also have to export the method sayHello() with @JSExport.

In general, things that should be exported on the top-level, such as top-level objects and classes, are exported with @JSExportTopLevel, while things that should be exported as properties or methods in JavaScript are exported with @JSExport.

Exporting top-level objects

Put on a top-level object, the @JSExportTopLevel annotation exports that object to the JavaScript global scope. The name under which it is to be exported must be specified as an argument to @JSExportTopLevel.

@JSExportTopLevel("HelloWorld")
object HelloWorld {
  ...
}

exports the HelloWorld object in JavaScript.

Exporting classes

The @JSExportTopLevel annotation can also be used to export Scala.js classes to JavaScript (but not traits), or, to be more precise, their constructors. This allows JavaScript code to create instances of the class.

@JSExportTopLevel("Foo")
class Foo(val x: Int) {
  override def toString(): String = s"Foo($x)"
}

exposes Foo as a constructor function to JavaScript:

var foo = new Foo(3);
console.log(foo.toString());

will log the string "Foo(3)" to the console. This particular example works because it calls toString(), which is always exported to JavaScript. Other methods must be exported explicitly as shown in the next section.

Exports with modules

When emitting a module for Scala.js code, top-level exports are not sent to the JavaScript global scope. Instead, they are genuinely exported from the module. In that case, an @JSExportTopLevel annotation has the semantics of an ECMAScript 2015 export. For example:

@JSExportTopLevel("Bar")
class Foo(val x: Int)

is semantically equivalent to this JavaScript export:

export { Foo as Bar };

Exporting methods

Similarly to objects, methods of Scala classes, traits and objects can be exported with @JSExport. Unlike for @JSExportTopLevel, the name argument is optional for @JSExport, and defaults to the Scala name of the method.

class Foo(val x: Int) {
  @JSExport
  def square(): Int = x*x // note the (), omitting them has a different behavior
  @JSExport("foobar")
  def add(y: Int): Int = x+y
}

Given this definition, and some variable foo holding an instance of Foo, you can call:

console.log(foo.square());
console.log(foo.foobar(5));
// console.log(foo.add(3)); // TypeError, add is not a member of foo

Overloading

Several methods can be exported with the same JavaScript name (either because they have the same name in Scala, or because they have the same explicit JavaScript name as parameter of @JSExport). In that case, run-time overload resolution will decide which method to call depending on the number and run-time types of arguments passed to the the method.

For example, given these definitions:

class Foo(val x: Int) {
  @JSExport
  def foobar(): Int = x
  @JSExport
  def foobar(y: Int): Int = x+y
  @JSExport("foobar")
  def bar(b: Boolean): Int = if (b) 0 else x
}

the following calls will dispatch to each of the three methods:

console.log(foo.foobar());
console.log(foo.foobar(5));
console.log(foo.foobar(false));

If the Scala.js compiler cannot produce a dispatching code capable of reliably disambiguating overloads, it will issue a compile error (with a somewhat cryptic message):

class Foo(val x: Int) {
  @JSExport
  def foobar(): Int = x
  @JSExport
  def foobar(y: Int): Int = x+y
  @JSExport("foobar")
  def bar(i: Int): Int = if (i == 0) 0 else x
}

gives:

[error] HelloWorld.scala:16: double definition:
[error] method $js$exported$meth$foobar:(i: Int)Any and
[error] method $js$exported$meth$foobar:(y: Int)Any at line 14
[error] have same type
[error]   @JSExport("foobar")
[error]    ^
[error] one error found

Hint to recognize this error: the methods are named $js$exported$meth$ followed by the JavaScript export name.

Exporting top-level methods

While an @JSExported method inside an @JSExportTopLevel object allows JavaScript code to call a “static” method, it does not feel like a top-level function from JavaScript’s point of view. @JSExportTopLevel can also be used directly on a method of a top-level object, which exports the method as a truly top-level function:

object A {
  @JSExportTopLevel("foo")
  def foo(x: Int): Int = x + 1
}

can be called from JavaScript as:

const y = foo(5);

Exporting properties

vals, vars and defs without parentheses, as well as defs whose name ends with _=, have a single argument and Unit result type, are exported to JavaScript as properties with getters and/or setters using, again, the @JSExport annotation.

Given this weird definition of a halfway mutable point:

@JSExport
class Point(_x: Double, _y: Double) {
  @JSExport
  val x: Double = _x
  @JSExport
  var y: Double = _y
  @JSExport
  def abs: Double = Math.sqrt(x*x + y*y)
  @JSExport
  def sum: Double = x + y
  @JSExport
  def sum_=(v: Double): Unit = y = v - x
}

JavaScript code can use the properties as follows:

var point = new Point(4, 10)
console.log(point.x);   // 4
console.log(point.y);   // 10
point.y = 20;
console.log(point.y);   // 20
point.x = 1;            // does nothing, thanks JS semantics
console.log(point.x);   // still 4
console.log(point.abs); // 20.396078054371138
console.log(point.sum); // 24
point.sum = 30;
console.log(point.sum); // 30
console.log(point.y);   // 26

As usual, explicit names can be given to @JSExport. For def setters, the JS name must be specified without the trailing _=.

def setters must have a result type of Unit and exactly one parameter. Note that several def setters with different types for their argument can be exported under a single, overloaded JavaScript name.

In case you overload properties in a way the compiler cannot disambiguate, the methods in the error messages will be prefixed by $js$exported$prop$.

Export fields directly declared in constructors

You can export fields directly declared in constructors by annotating the constructor argument:

class Point(
    @JSExport val x: Double,
    @JSExport val y: Double)

// Also applies to case classes
case class Point(
    @JSExport x: Double,
    @JSExport y: Double)

Export fields to the top level

Similarly to methods, fields (vals and vars) of top-level objects can be exported as top-level variables using @JSExportTopLevel:

object Foo {
  @JSExportTopLevel("bar")
  val bar = 42

  @JSExportTopLevel("foobar")
  var foobar = "hello"
}

exports bar and foobar to the top-level, so that they can be used from JavaScript as

console.log(bar);    // 42
console.log(foobar); // "hello"

Note that for vars, the JavaScript binding is read-only, i.e., JavaScript code cannot assign a new value to an exported var. However, if Scala.js code sets Foo.foobar, the new value will be visible from JavaScript. This is consistent with exporting a let binding in ECMAScript 2015 modules.

Automatically export all members

Instead of writing @JSExport on every member of a class or object, you may use the @JSExportAll annotation. It is equivalent to adding @JSExport on every public (term) member directly declared in the class/object:

class A {
  def mul(x: Int, y: Int): Int = x * y
}

@JSExportAll
class B(val a: Int) extends A {
  def sum(x: Int, y: Int): Int = x + y
}

This is strictly equivalent to writing:

class A {
  def mul(x: Int, y: Int): Int = x * y
}

class B(@(JSExport @field) val a: Int) extends A {
  @JSExport
  def sum(x: Int, y: Int): Int = x + y
}

It is important to note that this does not export inherited members. If you wish to do so, you’ll have to override them explicitly:

class A {
  def mul(x: Int, y: Int): Int = x * y
}

@JSExportAll
class B(val a: Int) extends A {
  override def mul(x: Int, y: Int): Int = super.mul(x,y)
  def sum(x: Int, y: Int): Int = x + y
}