Non-native JS types (aka Scala.js-defined JS types)
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.
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
:
Such classes are called non-native JS classes, and were previously known as Scala.js-defined JS classes.
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:
can be understood as the following ECMAScript 6 class definition (or its desugaring in ES 5.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:
has the same semantics as:
which, in ES 5.1, gives something like
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()
).
Unless overridden in a class or objects, concrete val
s, var
s and def
s declared
in 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:
has the same semantics as the following ECMAScript 2015 class:
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 JavaScript
and 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:
defines a JavaScript class Foo
with a variety of static members.
It can be understood as if defined in JavaScript as:
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:
and so is the following:
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:
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:
When calling ajax()
, we can now give an anonymous object that overrides only the val
s we care about:
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:
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:
However, it is thereafter impossible to access its members easily. The following does not work:
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
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:
Besides the run-time overhead incurred by such a resolution, this can cause weird problems if overloads are not mutually exclusive. For example:
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:
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:
Implicit expansion will desugar the above code into:
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.