Calling JavaScript from Kotlin

Kotlin was first designed for easy interoperation with the Java platform: it sees Java classes as Kotlin classes, and Java sees Kotlin classes as Java classes.

However, JavaScript is a dynamically typed language, which means it does not check types at compile time. You can freely talk to JavaScript from Kotlin via dynamic types. If you want to use the full power of the Kotlin type system, you can create external declarations for JavaScript libraries which will be understood by the Kotlin compiler and the surrounding tooling.

An experimental tool to automatically create Kotlin external declarations for npm dependencies which provide type definitions (TypeScript / d.ts) called Dukat is also available.

Inline JavaScript

You can inline some JavaScript code into your Kotlin code using the js("...") function. For example:

  1. fun jsTypeOf(o: Any): String {
  2. return js("typeof o")
  3. }

Because the parameter of js is parsed at compile time and translated to JavaScript code “as-is”, it is required to be a string constant. So, the following code is incorrect:

  1. fun jsTypeOf(o: Any): String {
  2. return js(getTypeof() + " o") // error reported here
  3. }
  4. fun getTypeof() = "typeof"

Note that invoking js() returns a result of type dynamic, which provides no type safety at compile time.

external modifier

To tell Kotlin that a certain declaration is written in pure JavaScript, you should mark it with the external modifier. When the compiler sees such a declaration, it assumes that the implementation for the corresponding class, function or property is provided externally (by the developer or via an npm dependency), and therefore does not try to generate any JavaScript code from the declaration. This is also why external declarations can’t have a body. For example:

  1. external fun alert(message: Any?): Unit
  2. external class Node {
  3. val firstChild: Node
  4. fun append(child: Node): Node
  5. fun removeChild(child: Node): Node
  6. // etc
  7. }
  8. external val window: Window

Note that the external modifier is inherited by nested declarations. This is why in the example Node class, we do not need to add the external modifier before member functions and properties.

The external modifier is only allowed on package-level declarations. You can’t declare an external member of a non-external class.

Declaring (static) members of a class

In JavaScript you can define members either on a prototype or a class itself:

  1. function MyClass() { ... }
  2. MyClass.sharedMember = function() { /* implementation */ };
  3. MyClass.prototype.ownMember = function() { /* implementation */ };

There is no such syntax in Kotlin. However, in Kotlin we have companion objects. Kotlin treats companion objects of external classes in a special way: instead of expecting an object, it assumes members of companion objects to be members of the class itself. MyClass from the example above can be described as follows:

  1. external class MyClass {
  2. companion object {
  3. fun sharedMember()
  4. }
  5. fun ownMember()
  6. }

Declaring optional parameters

If you are writing an external declaration for a JavaScript function which has an optional parameter, use definedExternally. This delegates the generation of the default values to the JavaScript function itself:

  1. external fun myFunWithOptionalArgs(
  2. x: Int,
  3. y: String = definedExternally,
  4. z: String = definedExternally
  5. )

With this external declaration, you can call myFunWithOptionalArgs with one required argument and two optional arguments, where the default values are calculated by the JavaScript implementation of myFunWithOptionalArgs.

Extending JavaScript classes

You can easily extend JavaScript classes as if they were Kotlin classes. Just define an external open class and extend it by a non-external class. For example:

  1. open external class Foo {
  2. open fun run()
  3. fun stop()
  4. }
  5. class Bar: Foo() {
  6. override fun run() {
  7. window.alert("Running!")
  8. }
  9. fun restart() {
  10. window.alert("Restarting")
  11. }
  12. }

There are some limitations:

  • When a function of an external base class is overloaded by signature, you can’t override it in a derived class.
  • You can’t override a function with default arguments.
  • Non-external classes can’t be extended by external classes.

external interfaces

JavaScript does not have the concept of interfaces. When a function expects its parameter to support two methods foo and bar, you would just pass in an object that actually has these methods.

You can use interfaces to express this concept in statically typed Kotlin:

  1. external interface HasFooAndBar {
  2. fun foo()
  3. fun bar()
  4. }
  5. external fun myFunction(p: HasFooAndBar)

A typical use case for external interfaces is to describe settings objects. For example:

  1. external interface JQueryAjaxSettings {
  2. var async: Boolean
  3. var cache: Boolean
  4. var complete: (JQueryXHR, String) -> Unit
  5. // etc
  6. }
  7. fun JQueryAjaxSettings(): JQueryAjaxSettings = js("{}")
  8. external class JQuery {
  9. companion object {
  10. fun get(settings: JQueryAjaxSettings): JQueryXHR
  11. }
  12. }
  13. fun sendQuery() {
  14. JQuery.get(JQueryAjaxSettings().apply {
  15. complete = { (xhr, data) ->
  16. window.alert("Request complete")
  17. }
  18. })
  19. }

External interfaces have some restrictions:

  • They can’t be used on the right-hand side of is checks.
  • They can’t be passed as reified type arguments.
  • They can’t be used in class literal expressions (such as I::class).
  • as casts to external interfaces always succeed. Casting to external interfaces produces the “Unchecked cast to external interface” compile time warning. The warning can be suppressed with the @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") annotation.

    IntelliJ IDEA can also automatically generate the @Suppress annotation. Open the intentions menu via the light bulb icon or Alt-Enter, and click the small arrow next to the “Unchecked cast to external interface” inspection. Here, you can select the suppression scope, and your IDE will add the annotation to your file accordingly.

Casting

In addition to the “unsafe” cast operator as, which throws a ClassCastException in case a cast is not possible, Kotlin/JS also provides unsafeCast<T>(). When using unsafeCast, no type checking is done at all during runtime. For example, consider the following two methods:

  1. fun usingUnsafeCast(s: Any) = s.unsafeCast<String>()
  2. fun usingAsOperator(s: Any) = s as String

They will be compiled accordingly:

  1. function usingUnsafeCast(s) {
  2. return s;
  3. }
  4. function usingAsOperator(s) {
  5. var tmp$;
  6. return typeof (tmp$ = s) === 'string' ? tmp$ : throwCCE();
  7. }