@JsExport guide for exposing Kotlin to JS
Tags: kotlinjs, kotlin, kotlinmultiplatform, javascript
Published: 2022-03-14 16:19:31

> Note that this post focuses on JS output for Kotlin. There is also a Typescript output (.d.ts file) with some unique issues that this post doesn't cover in detail.

In the previous [post](https://dev.to/touchlab/add-kotlinjs-support-to-your-kmm-library-48d9), we added `Kotlin/JS` support to an existing KMM library. Now, we would add code that works on the JS side.

## Table Of Contents
  * [Usage](#usage)
    * [@ExperimentalJsExport vs @JsExport](#annotations-vs)
  * [Limitations](#limitations)
    * [Collections](#limitation-collection)
    * [Long](#limitation-long)
    * [Interface](#limitation-interface)
      * [Solution - Using Implementation class](#limitation-interface-impl)
      * [Solution - Using Expect-Actual](#limitation-interface-expect)
    * [Enum](#limitation-enum)
    * [Sealed classes](#limitation-sealed)
    * [Code mangling](#limitation-mangling)
    * [Suspended functions](#limitation-suspended)

## Usage <a name="usage"></a>
It is critical to understand [@JsExport](https://kotlinlang.org/docs/js-to-kotlin-interop.html#jsexport-annotation) annotation and all the issues around it if you expose Kotlin code through Kotlin/JS as an external JS library

With the new [IR compiler](https://kotlinlang.org/docs/js-ir-compiler.html), **Kotlin declarations do not get exposed to JavaScript by default**. To make Kotlin declarations visible to JavaScript, they **must be** annotated with @JsExport.

> Note that @JsExport is experimental as of the posted date of this post (with Kotlin 1.6.10)

Let’s start with a very basic example,
```kotlin
// commonMain - Greeting.kt
class Greeting {
    fun greeting(): String {
        return "Hello World!"
    }
}
```
At this point, the generated `.js` library file would not have any reference to the Greeting class. The reason is that it is missing the `@JsExport` annotation.

> You can generate JS library code via `./gradlew jsBrowserDistribution`. You would find the `.js, .d.ts and map` file in `root/build/js/packages/<yourlibname>/kotlin` folder.

Now, add the annotation to generate JS code for it,

```kotlin
import kotlin.js.ExperimentalJsExport
import kotlin.js.JsExport

@ExperimentalJsExport
@JsExport
class Greeting {
    fun greeting(): String {
        return "Hello World!"
    }
}
```
The `.js` and `.d.ts` files would now contain the Greeting reference.

- **Generated .js file**
```javascript
function Greeting() {
}
Greeting.prototype.greeting = function () {
  return 'Hello World!';
};
Greeting.$metadata$ = {
  simpleName: 'Greeting',
  kind: 'class',
  interfaces: []
};
```

- **Generated .d.ts file**
```typescript
export namespace jabbar.jigariyo.kmplibrary {
    class Greeting {
        constructor();
        greeting(): string;
    }
}
```
Now you can call `Greeting` from JavaScript
```javascript
console.log(new jabbar.jigariyo.kmplibrary.Greeting().greeting())
// Hello World!
```
> Note that you would have to use fully qualified Kotlin names in JavaScript because Kotlin exposes its [package structure](https://kotlinlang.org/docs/js-to-kotlin-interop.html#package-structure) to JavaScript.

It is **important** to keep in mind that all public attributes in your exportable object would also need to be exportable.

In the following example, `CustomObj` would also need to be exportable to export `MyDataClass`,

```kotlin
@JsExport
data class MyDataClass(
    val strVal: String,
    val customObj: CustomObj // This would need to be exportable
)
```

### @ExperimentalJsExport vs @JsExport <a name="annotations-vs"></a>
`@JsExport` is the annotation you need to tell the compiler to generate JavaScript code, and `@ExperimentalJsExport` is an opt-in marker annotation to use `@JsExport` as it is experimental to use.

You can get rid of the requirement of adding `@ExperimentalJsExport` in code by declaring it as `OptIn` in `languageSettings` for all source sets in your `kotlin` block.

```kotlin
kotlin {
    sourceSets {
        all {
            languageSettings.apply {
                optIn("kotlin.js.ExperimentalJsExport")
            }
        }
    }
}
```
---
## Limitations <a name="limitations"></a>
As of Kotlin `1.6.10`, there are heavy limitations on what Kotlin types one can export to JavaScript.

You will most likely face one of these limitations if you add JS support in an existing KMP library.

Whenever something is `not-exportable`, you would get either an error or a warning:
- Code does not compile with such errors
![Js exportable error example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dzqmbsmgtl56c4u4syww.png)
- Code compiles with such warnings, but you might have run-time issues
![JS exportable warning](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h7kzdox841kzdl4heqoj.png)

### Collections <a name="limitation-collection"></a>
Kotlin's collections APIs are not exportable, so you would have to come up with different strategies to deal with them. Some examples would be:

#### Map
You would have to remove `Map` usage from `common` code that also exports to JS, or you would have to have a different implementation on the `mobile` and `js` side. You can use the `kotlin.js.Json` object on the `jsMain` side and then map it to the `Kotlin` map whenever needed.

For JS specific implementation, you may also look into using [Record](https://github.com/JetBrains/kotlin-wrappers/blob/master/kotlin-js/src/main/kotlin/kotlinx/js/Record.kt) from [kotlin-extensions](https://github.com/JetBrains/kotlin-wrappers/tree/master/kotlin-js) library.

#### List
You can replace the `List` usage with an `Array` to keep the same code for all platforms. It may or may not be a simple replacement.

For example, `Array` would work if only used in an object for parsing an API response. Note that having an `Array` in a `Data` class would require providing your own `equals` and `hashcode` implementations.

> Note that moving from `List` to `Array` might have an impact on generated code for `iOS`. `List` becomes `NSArray` on iOS side but `Array` becomes a Kotlin object wrapping the array

If you want separate implementation for `jsMain`, then `kotlin-extensions` library provides some helpful JS specific classes like [Iterator, Set, and ReadOnlyArray](https://github.com/JetBrains/kotlin-wrappers/tree/master/kotlin-js/src/main/kotlin/kotlinx/js)

### Long <a name="limitation-long"></a>
`Long` is not mapped to anything as there is no equivalent in the `JavaScript` world. You would see the `non-exportable` warning if you export `Long` via `Kotlin`.

If you ignore the warning, then `Long` still kinda works. It just takes any value from JS. Kotlin will receive the input as `Long` if JavaScript code sends a `BigInt`.

It will not work for `Typescript` unless you set `skipLibCheck = true` in the config as type `kotlin.Long` is not available.

```kotlin
// Kotlin 
@JsExport
class Greeting {
    @Suppress("NON_EXPORTABLE_TYPE")
    fun printLong(value: Long) {
        print(value)
    }
}

// Generated .js
Greeting.prototype.printLong = function (value) {
  print(value);
  };

// Generated .d.ts
printLong(value: kotlin.Long): void;

// Usage from JS
const value = "0b11111111111111111111111111111111111111111111111111111"
Greeting().printLong(BigInt(value)) // This works

```
> You can use @Suppress("NON_EXPORTABLE_TYPE") to suppress the exportable warning
  
### Interface <a name="limitation-interface"></a>
Kotlin interfaces are not exportable. It gets annoying when a library has an interface-driven design, where it exposes the interface in public API rather than a specific implementation.

> Interfaces will be exportable starting upcoming [Kotlin 1.6.20](https://youtrack.jetbrains.com/issue/KT-45434)! We would have to play around with that to see it working.

There are workarounds to make interfaces work on `JavaScript`.

The following are some examples for getting around interfaces:
#### Using Implementation Class <a name="limitation-interface-impl"></a>

```kotlin
@JsExport
interface HelloInterface {
    fun hello()
}
```

The above code would show the non-exportable error. You can use the `interface` indirectly via its implementation class to work around that problem.

```kotlin
@JsExport
object Hello : HelloInterface {
    override fun hello() {
        console.log("HELLO from HelloInterface")
    }
}
```
> Generated JS code for the above `hello` method will have a `mangled` name. Read more about it in [code-mangling](#limitation-mangling) section

```kotlin
interface HelloInterface {
    @JsName("hello")
    fun hello()
}

@JsExport
object Hello : HelloInterface {
    override fun hello() {
        console.log("HELLO from HelloInterface")
    }
}
```

Similarly, here are some variations to use `HelloInterface`,

```kotlin
// Variation (2)
@JsExport
object HelloGet {
    fun getInterface(): HelloInterface {
        return Hello
    }
}

// Variation (3)
@JsExport
class HelloWrapper(@JsName("value") val value: HelloInterface)

// Variation (4)
@JsExport
data class HelloWrapperData(@JsName("value") val value: HelloInterface)

```

All above variations are usable from the `JS` side even with a `non-exportable` warning around interface usage,

```javascript
/**
 * JS side calling code
 * (1)
 * Hello.hello()
 *
 * (2)
 * HelloGet.getInterface().hello()
 *
 * (3)
 * const wrapperObj = HelloWrapper(Hello)
 * wrapperObj.value.hello()
 *
 * (4)
 * const wrapperDataObj = HelloWrapperData(Hello)
 * wrapperDataObj.value.hello()
 */
```

#### Using Expect-Actual Pattern <a name="limitation-interface-expect"></a>

Another idea for using interfaces is to use the `expect-actual` pattern to define a Kotlin interface in `common` and `mobile` platforms and define an `external interface` for the JS side. This approach might not scale well but can be very useful for simple cases.

```kotlin
// commonMain
expect interface Api {
    fun getProfile(callback: (Profile) -> Unit)
}

// jsMain
// Here external makes it a normal JS object in generated code
actual external interface Api {
    actual fun getProfile(callback: (Profile) -> Unit)
}

// mobileMain
actual interface Api {
    actual fun getProfile(callback: (Profile) -> Unit)
}
```

These examples showcase workarounds that might or might not work for a particular project. 

### Enum <a name="limitation-enum"></a>
As of Kotlin 1.6.10, enums are not exportable. It can create issues for projects that have a lot of existing enums. 

> Good news is that its support coming in Kotlin 1.6.20

There is also a trick to export and use enums on JS. It requires defining a JS-specific object with attributes that point to actual enums.

For example, this code won't compile,

```kotlin
@JsExport
enum Gender {
    MALE,
    FEMALE
}
```

Instead, you can do this indirectly by re-defining them through object fields. It works with a non-exportable warning. Note the warning suppression with annotation.

```kotlin
@Suppress("NON_EXPORTABLE_TYPE")
@ExperimentalJsExport
@JsExport
object GenderType {
    val male = Gender.MALE
    val female = Gender.FEMALE
}
```

### Sealed classes <a name="limitation-sealed"></a>
Sealed classes are exportable, but they’re buggy as of Kotlin 1.6.10

You can export a data or regular class as subclasses inside a Sealed class body, but not an object. 

```kotlin
@JsExport
sealed class State {
    object Loading: State() // This won't be visible 
    data class Done(val value: String): State() // This would be visible
}
```

You can work around this problem by moving the subclasses outside the body of the sealed class, but then you cannot write it like `State.Loading`. It is more of a readability issue in that case.

Also, sealed classes have known issues with typescript binding as well.

### Code mangling <a name="limitation-mangling"></a>
The Kotlin compiler mangles the names of the functions and attributes. It can be frustrating to deal with mangled names.

For example,
```kotlin
@JsExport
object Hello : HelloInterface {
    override fun hello() {
        console.log("HELLO from HelloInterface")
    }
}
```
Generated JS code for `hello` method looks like,
```javascript
Hello.prototype.hello_sv8swh_k$ = function () {
  console.log('HELLO from HelloInterface');
};
```
We would need to use the [@JsName](https://kotlinlang.org/docs/js-to-kotlin-interop.html#jsname-annotation) annotation to provide a generated name. If you see numbers in attribute names like `_something_0, _value_3` on the JS side, then it is a sign that you need to provide a controlled name via `@JsName` annotation on the `Kotlin` side.

After adding `@JsName("hello")` in the above example, generated code looks like this where there is a new `hello` method that references `hello_sv8swh_k$` internally,

```javascript
Hello.prototype.hello_sv8swh_k$ = function () {
  console.log('HELLO from HelloInterface');
};
Hello.prototype.hello = function () {
  return this.hello_sv8swh_k$();
};
```

> Note that `@JsName` is prohibited for overridden members, so you would need to set it to a base class property or method.

### Suspended functions <a name="limitation-suspended"></a>
You cannot expose suspended functions to JS. You would need to convert them into `JavaScript Promise` object.

The easiest way to do that would be to wrap suspend calls inside,
```
GlobalScope.promise {
  // suspend call
}
```
This function comes from `Promise.kt` in the `coroutine library`. It returns a generic type.

---
As mentioned earlier, some of these issues would get resolved with Kotlin 1.6.20, so keep that in mind.

---

_In the next post, we will look at different ways to distribute Kotlin/JS library since we've some JS exportable code._

Thanks for reading! Let me know in the comments if you have questions. Also, you can reach out to me at @[shaktiman_droid](https://twitter.com/shaktiman_droid) on Twitter, [LinkedIn](https://www.linkedin.com/in/shaktiman-droid/) or [Kotlin Slack](https://slack.kotlinlang.org/). And if you find all this interesting, maybe you'd like to [work with](https://touchlab.co/contact-us/) or [work at](https://touchlab.co/careers-3/) Touchlab.