A non-native JS type, aka Scala.js-defined JS type, is a JavaScript type implemented in Scala.js code.This is in contrast to native JS types, described in the facade types reference, which represent APIs implemented in JavaScript code.
About @ScalaJSDefined
In Scala.js 0.6.x, the @ScalaJSDefined
is necessary to declare a non-native JS type, also called a Scala.js-defined JS type.Starting from Scala.js 1.x however, the annotation is not necessary anymore.Since Scala.js 0.6.17, you can opt-in for the new semantics of 1.x where @ScalaJSDefined
is not necessary, by giving the option -P:scalajs:sjsDefinedByDefault
to scalac.In an sbt build, this is done with
scalacOptions += "-P:scalajs:sjsDefinedByDefault"
The present documentation assumes that you are using this option (or Scala.js 1.x).Code snippets mention the necessary @ScalaJSDefined
in comments as a reference for older versions.
Defining a non-native JS type
Any class, trait or object that inherits from js.Any
is a JS type.Often, it will extend js.Object
which itself extends js.Any
:
import scala.scalajs.js
import scala.scalajs.js.annotation._
// @ScalaJSDefined
class Foo extends js.Object {
val x: Int = 4
def bar(x: Int): Int = x + 1
}
Such classes are called non-native JS classes, and are also known as Scala.js-defined JS classes (especially in 0.6.x).All their members are automatically visible from JavaScript code.The class itself (its constructor function) is not visible by default, but can be exported with @JSExportTopLevel
.Moreover, they can extend JavaScript classes (native or not), and, if exported, be extended by JavaScript classes.
Being JavaScript types, the Scala semantics do not apply to these classes.Instead, JavaScript semantics apply.For example, overloading is dispatched at run-time, instead of compile-time.
Restrictions
Non-native JS types have the following restrictions:
- Private methods cannot be overloaded.
- Qualified private members, i.e.,
private[EnclosingScope]
, must befinal
. - Non-native JS classes, traits and objects cannot directly extend native JS traits (it is allowed to extend a native JS class).
- Non-native JS traits cannot declare concrete term members (i.e., they must all be abstract) unless their right-hand-side is exactly
= js.undefined
. - Non-native JS classes and objects must extend a JS class, for example
js.Object
(they cannot directly extendAnyRef with js.Any
). - Declaring a method named
apply
without@JSName
is illegal. - Declaring a method with
@JSBracketSelect
or@JSBracketCall
is illegal. - Mixing fields, pairs of getter/setter, and/or methods with the same name is illegal. (For example
def foo: Int
anddef foo(x: Int): Int
cannot both exist in the same class.)
Semantics
What JavaScript sees
val
s andvar
s become actual JavaScript fields of the object, so JavaScript sees a field stored on the object.def
s with()
become JavaScript methods on the prototype.def
s without()
become JavaScript getters on the prototype.def
s whose Scala name ends with=
become JavaScript _setters on the prototype.
In other words, the following definition:
// @ScalaJSDefined
class Foo extends js.Object {
val x: Int = 5
var y: String = "hello"
def z: Int = 42
def z_=(v: Int): Unit = println("z = " + v)
def foo(x: Int): Int = x + 1
}
can be understood as the following ECMAScript 6 class definition (or its desugaring in ES 5.1):
class Foo extends global.Object {
constructor() {
super();
this.x = 5;
this.y = "hello";
}
get z() {
return 42;
}
set z(v) {
console.log("z = " + v);
}
foo(x) {
return x + 1;
}
}
The JavaScript names are the same as the field and method names in Scala by default.You can override this with @JSName("customName")
.
private
, private[this]
and private[EnclosingScope]
methods, getters and setters are not visible at all from JavaScript.Private fields, however, will exist on the object, with unpredictable names.Trying to access them is undefined behavior.
All other members, including protected ones, are visible to JavaScript.
super calls
super
calls have the semantics of super
references in ECMAScript 6.For example:
// @ScalaJSDefined
class Foo extends js.Object {
override def toString(): String = super.toString() + " in Foo"
}
has the same semantics as:
class Foo extends global.Object {
toString() {
return super.toString() + " in Foo";
}
}
which, in ES 5.1, gives something like
Foo.prototype.toString = function() {
return global.Object.prototype.toString.call(this) + " in Foo";
};
For fields, getters and setters, the ES 6 spec is a bit complicated, but it essentially “does the right thing”.In particular, calling a super getter or setter works as expected.
Non-native JS object
A non-native JS object
is a singleton instance of a non-native JS class.There is nothing special about this, it’s just like Scala objects.
Non-native JS objects are not automatically visible to JavaScript.They can be exported with @JSExportTopLevel
, just like Scala object: they will appear as a 0-argument function returning the instance of the object.
Non-native JS traits
Traits and interfaces do not have any existence in JavaScript.At best, they are documented contracts that certain classes must satisfy.So what does it mean to have native and non-native JS traits?
Native JS traits can only be extended by native JS classes, objects and traits.In other words, a non-native JS class/trait/object cannot extend a native JS trait.They can only extend non-native JS traits.
Term members (val
s, var
s and def
s) in non-native JS traits must:
- either be abstract,
- or have
= js.undefined
as right-hand-side (and not be adef
with()
).
// @ScalaJSDefined
trait Bar extends js.Object {
val x: Int
val y: Int = 5 // illegal
val z: js.UndefOr[Int] = js.undefined
def foo(x: Int): Int
def bar(x: Int): Int = x + 1 // illegal
def foobar(x: Int): js.UndefOr[Int] = js.undefined // illegal
def babar: js.UndefOr[Int] = js.undefined
}
Unless overridden in a class or objects, concrete val
s, var
s and def
s declaredin a JavaScript trait (necessarily with = js.undefined
) are not exposed to JavaScript at all.For example, implementing (the legal parts of) Bar
in a subclass:
// @ScalaJSDefined
class Babar extends Bar {
val x: Int = 42
def foo(x: Int): Int = x + 1
override def babar: js.UndefOr[Int] = 3
}
has the same semantics as the following ECMAScript 2015 class:
class Babar extends global.Object { // `extends Bar` disappears
constructor() {
super();
this.x = 42;
}
foo(x) {
return x + 1;
}
get babar() {
return 3;
}
}
Note that z
is not defined at all, not even as this.z = undefined
.The distinction is rarely relevant, because babar.z
will return undefined
in JavaScriptand in Scala.js if babar
does not have a field z
.
Static members
When defining a non-native JS class (not a trait nor an object), it is also possible to define static members.Static members must be defined in the companion object of the class, and annotated with @JSExportStatic
.For example:
// @ScalaJSDefined
class Foo extends js.Object
object Foo {
@JSExportStatic
val x: Int = 5
@JSExportStatic
var y: String = "hello"
@JSExportStatic
def z: Int = 42
@JSExportStatic
def z_=(v: Int): Unit = println("z = " + v)
@JSExportStatic
def foo(x: Int): Int = x + 1
}
defines a JavaScript class Foo
with a variety of static members.It can be understood as if defined in JavaScript as:
class Foo extends global.Object {
static get z() {
return 42;
}
static set z(v) {
console.log("z = " + v);
}
static foo(x) {
return x + 1;
}
}
Foo.x = 5;
Foo.y = "hello";
Note that JavaScript doesn’t have any declarative syntax for static fields, hence the two imperative assignments at the end.
Restrictions
- The companion object must be a Scala object, i.e., it cannot extend
js.Any
. lazy val
s cannot be marked with@JSExportStatic
- Static fields (
val
s andvar
s) must be defined before any other (non-static) field, as well as before any constructor statement.
As an example of the last bullet, the following snippet is illegal:
// @ScalaJSDefined
class Foo extends js.Object
object Foo {
val x: Int = 5
@JSExportStatic
val y: String = "hello" // illegal, defined after `x` which is non-static
}
and so is the following:
// @ScalaJSDefined
class Foo extends js.Object
object Foo {
println("Initializing Foo")
@JSExportStatic
val y: String = "hello" // illegal, defined after the `println` statement
}
Anonymous classes
Anonymous JS classes are particularly useful to create typed object literals, in the presence of a non-native JS trait describing an interface.For example:
// @ScalaJSDefined
trait Position extends js.Object {
val x: Int
val y: Int
}
val pos = new Position {
val x = 5
val y = 10
}
Note that anonymous classes extending js.Any
are always non-native, even in 0.6.x without -P:scalajs:sjsDefinedByDefault
nor @ScalaJSDefined
.
Use case: configuration objects
For configuration objects that have fields with default values, concrete members with = js.undefined
can be used in the trait.For example:
// @ScalaJSDefined
trait JQueryAjaxSettings extends js.Object {
val data: js.UndefOr[js.Object | String | js.Array[Any]] = js.undefined
val contentType: js.UndefOr[Boolean | String] = js.undefined
val crossDomain: js.UndefOr[Boolean] = js.undefined
val success: js.UndefOr[js.Function3[Any, String, JQueryXHR, _]] = js.undefined
...
}
When calling ajax()
, we can now give an anonymous object that overrides only the val
s we care about:
jQuery.ajax(someURL, new JQueryAjaxSettings {
override val crossDomain: js.UndefOr[Boolean] = true
override val success: js.UndefOr[js.Function3[Any, String, JQueryXHR, _]] = {
js.defined { (data: Any, textStatus: String, xhr: JQueryXHR) =>
println("Status: " + textStatus)
}
}
})
Note that for functions, we use js.defined { … }
to drive Scala’s type inference.Otherwise, it needs to apply two implicit conversions, which is not allowed.
The explicit types are quite annoying, but they are only necessary in Scala 2.10 and 2.11.If you use Scala 2.12, you can omit all the type annotations (but keep js.defined
), thanks to improved type inference for val
s and SAM conversions:
jQuery.ajax(someURL, new JQueryAjaxSettings {
override val crossDomain = true
override val success = js.defined { (data, textStatus, xhr) =>
println("Status: " + textStatus)
}
})
Caveat with reflective calls
It is possible to define an object literal with the anonymous class syntax without the support of a super class or trait defining the API, like this:
val pos = new js.Object {
val x = 5
val y = 10
}
However, it is thereafter impossible to access its members easily.The following does not work:
println(pos.x)
This is because pos
is a structural type in this case, and accessing x
is known as a reflective call in Scala.Reflective calls are not supported on values with JavaScript semantics, and will fail at runtime.Fortunately, the compiler will warn you against reflective calls, unless you use the relevant language import.
Our advice: do not use the reflective calls language import.
Run-time overloading
// @ScalaJSDefined
class Foo extends js.Object {
def bar(x: String): String = "hello " + x
def bar(x: Int): Int = x + 1
}
val foo = new Foo
println(foo.bar("world")) // choose at run-time which one to call
Even though typechecking will resolve to the first overload at compile-time to decide the result type of the function, the actual call will re-resolve at run-time, using the dynamic type of the parameter. Basically something like this is generated:
// @ScalaJSDefined
class Foo extends js.Object {
def bar(x: Any): Any = {
x match {
case x: String => "hello " + x
case x: Int => x + 1
}
}
}
Besides the run-time overhead incurred by such a resolution, this can cause weird problems if overloads are not mutually exclusive.For example:
// @ScalaJSDefined
class Foo extends js.Object {
def bar(x: String): String = bar(x: Any)
def bar(x: Any): String = "bar " + x
}
val foo = new Foo
println(foo.bar("world")) // infinite recursion
With compile-time overload resolution, the above would be fine, as the call to bar(x: Any)
resolves to the second overload, due to the static type of Any
.With run-time overload resolution, however, the type tests are executed again, and the actual run-time type of the argument is still String
, which causes an infinite recursion.
Goodies
js.constructorOf[C]
To obtain the JavaScript constructor function of a JS class (native or not) without instantiating it nor exporting it, you can use js.constructorOf[C]
, whose signature is:
package object js {
def constructorOf[C <: js.Any]: js.Dynamic = <stub>
}
C
must be a class type (i.e., such that you can give it to classOf[C]
) and refer to a JS class (not a trait nor an object).The method returns the JavaScript constructor function (aka the class value) for C
.
This can be useful to give to JavaScript libraries expecting constructor functions rather than instances of the classes.
js.ConstructorTag[C]
js.ConstructorTag[C]
is to js.constructorOf[C]
as ClassTag[C]
is to classOf[C]
, i.e., you can use an implicit
parameter of type js.ConstructorTag[C]
to implicitly get a js.constructorOf[C]
.For example:
def instantiate[C <: js.Any : js.ConstructorTag]: C =
js.Dynamic.newInstance(js.constructorTag[C].constructor)().asInstanceOf[C]
val newEmptyJSArray = instantiate[js.Array[Int]]
Implicit expansion will desugar the above code into:
def instantiate[C <: js.Any](implicit tag: js.ConstructorTag[C]): C =
js.Dynamic.newInstance(tag.constructor)().asInstanceOf[C]
val newEmptyJSArray = instantiate[js.Array[Int]](
new js.ConstructorTag[C](js.constructorOf[js.Array[Int]]))
although you cannot write the desugared version in user code because the constructor of js.ConstructorTag
is private.
This feature is particularly useful for Scala.js libraries wrapping JavaScript frameworks expecting to receive JavaScript constructors as parameters.