article banner

JavaScript interoperability

This is a chapter from the book Advanced Kotlin. You can find it on LeanPub or Amazon.

Let's say you have a Kotlin/JVM project and realize you need to use some parts of it on a website. This is not a problem: you can migrate these parts to a common module that can be used to build a package to be used from JavaScript or TypeScript0. You can also distribute this package to npm and expose it to other developers.

The first time I encountered such a situation was with AnkiMarkdown1, a library I built that lets me keep my flashcards in a special kind of Markdown and synchronize it with Anki (a popular program for flashcards). I initially implemented this synchronization as a JVM application I ran in the terminal, but this was inconvenient. Since I use Obsidian to manage my notes, I realized using AnkiMarkdown as an Obsidian plugin would be great. For that, I needed my synchronization code in JS. Not a problem! It took only a couple of hours to move it to the multiplatform module and distribute it to npm, and now I can use it in Obsidian.

The second time I had a similar situation was when I worked for Scanz. We had a desktop client for a complex application implemented in Kotlin/JVM. Since we wanted to create a new web-based client, we extracted common parts (services, view models, repositories) into a shared module and reused it. There were some trade-offs we needed to make, as I will show in this chapter, but we succeeded, and not only was it much faster than rewriting all these parts, but we could also use the common module for Android and iOS applications.

The last story I want to share is my Sudoku problem generator and solver2. It implements all the basic sudoku techniques and uses them to generate and solve sudoku puzzles. I needed it for a book that teaches how to solve sudoku. To present these sudoku problems and solve them myself conveniently, I implemented a React application. Then, I turned my Sugoku generator and solver into a common module and distributed it to npm.

In this chapter, I would like to share everything I learned along the way. I will show you the challenges when we transform a Kotlin/JVM project to use it conveniently from TypeScript and how to use the package produced this way in practice.

When I was writing this chapter, I was using Kotlin version 1.8.21. Technically speaking, Kotlin/JS should be stable since 1.8.0, but I would not be surprised if some details of what I am explaining in this chapter (like Gradle task names) change in the next versions.

Setting up a project

The first thing we need to do is set up a multiplatform project. We need a Gradle module using the Kotlin Multiplatform plugin, with js configured in the kotlin block. To specify the output format, we should call browser, nodejs or useEsModules in the js block, depending on where we want to run our code. If you want to run your code in browser applications, like in the examples from the introduction to this chapter, use the browser block. If you want to generate a NodeJS module, use nodejs. To generate ES6 modules, use useEsModules. Finally, you can call binaries.executable() in the js block, which explicitly instructs the Kotlin compiler to emit executable .js files. We should also specify jsMain (and jsTest if we want to write JS-specific tests) in the sourceSets block.

plugins {
   kotlin("multiplatform") version "1.8.21"
}

kotlin {
   jvm {}
   js(IR) {
       browser() // use if you need to run code in a browser
       nodejs() // use if you need to run code in a Node.js
       useEsModules() // output .mjs ES6 modules
       binaries.executable()
   }
   sourceSets {
       val commonMain by getting {
           dependencies {
               // common dependencies
           }
       }
       val commonTest by getting
       val jvmMain by getting
       val jvmTest by getting
       val jsMain by getting
       val jsTest by getting
   }
}

The IR in the js block means that the Intermediate Representation compiler will be used, which is a modern standard because of its source map generation and the JS optimizations it introduces; moreover, in the future it might help support Kotlin/JS debugging.

Currently, the kotlin("js") plugin is deprecated, and we should only use kotlin("multiplatform") to target JavaScript.

Now that we know how to set up a Kotlin/JS project let's see what it lets us do.

Using libraries available for Kotlin/JS

Not all dependencies are multiplatform and have support for Kotlin/JS. If a certain dependency is not multiplatform, you must find an alternative or implement platform-specific implementations yourself. To avoid this, it is best to use truly multiplatform libraries in the first place, especially those provided by the Kotlin team, like Kotlinx Serialization, Coroutines, and Ktor Client.

Using Kotlin/JS

Using Kotlin/JS is very similar to using Kotlin/JVM as we can use all Kotlin features, including standard library elements. However, a standard Java library is not available, so we have to use functions provided by JavaScript instead. For instance, we can use console.log("Hello") to print a message to the console.

fun printHello() { console.log("Hello") }

This is possible because Kotlin/JS provides a set of declarations for JavaScript functions and objects. For instance, console is declared as a top-level property of type Console. This type has a log function that accepts any values as arguments.

@Suppress("NOT_DOCUMENTED") public external interface Console { public fun dir(o: Any): Unit public fun error(vararg o: Any?): Unit public fun info(vararg o: Any?): Unit public fun log(vararg o: Any?): Unit public fun warn(vararg o: Any?): Unit } public external val console: Console

Both the console property and the Console interface are declared as external, which means they are not implemented in Kotlin, but JavaScript provides them. We can use them in our Kotlin code but not implement them. If there is a JavaScript element we want to use in Kotlin but don’t have a declaration for, we can create it ourselves. For instance, if we want to use the alert function, we can declare it as follows:

fun showAlert() { alert("Hello") } @JsName("alert") external fun alert(message: String)

Sometimes our external declarations need to be nested and complex, as I will show in the Adding npm dependencies section.

Using the js function, we can also call JavaScript elements from Kotlin/JS without explicit declarations. It executes JavaScript code defined as a string and returns its result. For instance, we can use it to call the prompt function:

fun main() { val message = js("prompt('Enter your name')") println(message) }

The string used as an argument for the js function must be known at compile time and must be a raw string without any operations. However, inside it we can use local variables defined in Kotlin. For instance, we can use the user variable in the following code:

fun main() { val user = "John" val surname = js("prompt('What is your surname ${user}?')") println(surname) }

The result of js is dynamic, which is a special type for Kotlin/JS that is not checked by the compiler and represents any JavaScript value. This means we can assign it to any variable, call any function on it, and cast it to any type. If a cast is not possible, it will throw an exception at runtime. For instance, we can cast js("1") to Int.

fun main() { val o: dynamic = js("{name: 'John', surname: 'Foo'}") println(o.name) // John println(o.surname) // Foo println(o.toLocaleString()) // [object Object] println(o.unknown) // undefined val i: Int = js("1") println(i) // 1 }

We can create JavaScript objects in Kotlin using the json function, which expects a vararg of pairs of String and Any? and returns an object of type Json. To create a nested object, use the json function with arguments representing key-value pairs.

import kotlin.js.json fun main() { val o = json( "name" to "John", "age" to 42, ) print(JSON.stringify(o)) // {"name":"John","age":42} }

These are the essentials of Kotlin/JS-specific structures. Now, let's focus on what we can do with them.

Building and linking a package

We have our multiplatform module configured with a Kotlin/JS target or just a Kotlin/JS module, and we want to use it in a JavaScript or TypeScript project. For that, we need to build a package. A production-ready package for the browser can be built using the jsBrowserProductionLibraryDistribution Gradle task, the result of which is a .js file and a .d.ts file within your common module's build/productionLibrary directory. The default name of these files is the name of the module, or it can be specified using the moduleName property in the js block inside the kotlin block.

For example, if your module is called common, you should use the :common:jsBrowserProductionLibraryDistribution task, and the result will be common.js and common.d.ts files inside the common/build/productionLibrary directory.

If your project defines no modules and therefore only has a top-level Gradle file, the module's name will be the project's name. In the AnkiMarkdown project, for instance, the result of the jsBrowserProductionLibraryDistribution task is ankimarkdown.js and ankimarkdown.d.ts files inside /build/productionLibrary directory.

The next step is to link this package to your JavaScript or TypeScript project. We could just copy-paste the generated files, but this wouldn’t be very convenient because we would need to repeat this process every time the common module changed. A better way is to link the generated files.

I assume we’ll use npm or yarn to manage our JavaScript or TypeScript project. In this case, we should define a dependency on a common module package in the package.json file. We can do this by adding a line to the dependencies section that defines the path to the directory containing our package, starting from "file:". Use relative paths. This is what it might look like in our example projects:

// A project where common module is called common
"dependencies": {
   // ...
   "common": "file:../common/build/productionLibrary"
}

// AnkiMarkdown project
"dependencies": {
   // ...
   "AnkiMarkdown": "file:../build/productionLibrary"
}

To avoid reinstalling our dependencies whenever we build a new package from our common module, we should now link our module using npm or yarn linking. To link a module defined in a file, go to that file location first (productionLibrary folder) and call npm link or yarn link. Then, go to your JavaScript or TypeScript project and call npm link <module-name> or yarn link <module-name>. When you subsequently start your project, you should see changes in the dependency immediately after building a new package using the jsBrowserProductionLibraryDistribution task.

Distributing a package to npm

We can also distribute our Kotlin module as a package to npm. Currently, it seems to be standard to use the dev.petuska.npm.publish3 plugin for this. You need to configure your npm package inside the npmPublish block, set the name and version, and register the npm registry. This is what it currently looks like in the AnkiMarkdown project (before using this plugin, I recommend checking its documentation and alternatives):

npmPublish {
   packages {
       named("js") {
           packageName.set("anki-markdown")
           version.set(libVersion)
       }
   }
   registries {
       register("npmjs") {
           uri.set(uri("https://registry.npmjs.org"))
           authToken.set(npmSecret)
       }
   }
}

npmSecret is a string containing the npm token needed to publish your package. You can get it from your npm account.

Exposing objects

All public elements from a Kotlin/JS module can be used in another Kotlin/JS module, but we need to expose them if we want to use them in JavaScript or TypeScript. For that, we need to use the @JsExport annotation, which can be used on classes, objects, functions, properties, and top-level functions and properties. Elements that are not exported will not appear in the .d.ts file, and their methods and property names will be mangled in the .js file.

Consider the following file in Kotlin/JS:

@JsExport
class A(
   val b: Int
) {
   fun c() { /*...*/ }
}

@JsExport
fun d() { /*...*/ }

class E(
   val h: String
) {
   fun g() { /*...*/ }
}

fun i() { /*...*/ }

The .d.ts result of the jsBrowserProductionLibraryDistribution task will be:

type Nullable<T> = T | null | undefined
export class A {
   constructor(b: number);
   get b(): number;
   c(): void;
}
export function d(): void;
export as namespace AnkiMarkdown;

There is no sign of class E or function i. If elements are used in other parts of our code, they will be present in the .js file, but property and function names will be mangled4, so we cannot use them in JavaScript or TypeScript.

This is a huge limitation because we cannot use any Kotlin class that is not exported. The simplest solution would be to mark our common module elements as @JsExport,but there is a problem with this. We cannot use the Kotlin standard library elements in JavaScript or TypeScript because many of its classes are not exported, including kotlin.collections.List. Instead, we should use arrays. Another problematic type is Long because all other types of numbers are transformed to JavaScript number type, but Long is non-exportable5.

As a consequence, we have two options:

  • Mark common module classes as JsExport and adjust them to the limitations. This, for instance, means we need to use arrays instead of lists, and we cannot use the Long type. Such changes might not be acceptable when JavaScript is not the primary target of our project.
  • Create wrappers for all classes we want to use in JavaScript or TypeScript. This option is considered better for projects that are not built primarily for Kotlin/JS. This is what such wrappers could look like:
@JsExport @JsName("SudokuGenerator") class SudokuGeneratorJs { private val sudokuGenerator = SudokuGenerator() fun generate(): SudokuJs { return SudokuJs(sudokuGenerator.generate()) } } @JsExport @JsName("Sudoku") class SudokuJs internal constructor( private val sudoku: Sudoku ) { fun valueAt(position: PositionJs): Int { return sudoku.valueAt(position.toPosition()) } fun possibilitiesAt(position: PositionJs): Array<Int> { return sudoku.possibilitiesAt(position.toPosition()) .toTypedArray() } fun isSolved(): Boolean { return sudoku.isSolved() } } @JsExport @JsName("Position") class PositionJs( val row: Int, val column: Int ) fun PositionJs.toPosition() = Position( row = row, column = column ) fun Position.toPositionJs() = PositionJs( row = row, column = column )

JsName annotation is used to change the name of an element in JavaScript. We often use JsName for wrapper classes to give them the same name as the original class, but without the JS suffix. We also sometimes use it to prevent mangling of method or property names.

This is how TypeScript declarations will look:

type Nullable<T> = T | null | undefined
export class SudokuGenerator {
 constructor();
 generate(): Sudoku;
}
export class Sudoku {
 private constructor();
 valueAt(position: Position): number;
 possibilitiesAt(position: Position): Array<number>;
 isSolved(): boolean;
}
export class Position {
 constructor(row: number, column: number);
 get row(): number;
 get column(): number;
}
export as namespace Sudoku;

It is likely that, one day, there will be KSP or compiler plugin libraries to generate such wrappers automatically, but right now we need to create them manually.

Exposing Flow and StateFlow

Another common problem with using Kotlin code from JavaScript is that types from the Kotlin Coroutines library, like Flow and StateFlow, which in MVVM architecture are used to represent state and data streams, are not exported. Consider that you have the following class in your common module:

class UserListViewModel( private val userRepository: UserRepository ) : ViewModel() { private val _userList: MutableStateFlow<List<User>> = MutableStateFlow(emptyList()) val userList: StateFlow<List<User>> = _userList private val _error: MutableStateFlow<Throwable?> = MutableSharedFlow() val error: Flow<Throwable?> = _error fun loadUsers() { viewModelScope.launch { userRepository.fetchUsers() .onSuccess { _usersList.value = it } .onFailure { _error.emit(it) } } } }

This object could not be used in JavaScript even if it were exported because it uses the StateFlow and Flow types, which are not exported, so we need to make a wrapper for them. The Flow type represents an observable source of values. A value that wraps it could provide a startObserving method to observe its events, and a stopObserving method to stop all observers.

@JsExport interface FlowObserver<T> { fun stopObserving() fun startObserving( onEach: (T) -> Unit, onError: (Throwable) -> Unit = {}, onComplete: () -> Unit = {}, ) } fun <T> FlowObserver( delegate: Flow<T>, coroutineScope: CoroutineScope ): FlowObserver<T> = FlowObserverImpl(delegate, coroutineScope) class FlowObserverImpl<T>( private val delegate: Flow<T>, private val coroutineScope: CoroutineScope ) : FlowObserver<T> { private var observeJobs: List<Job> = emptyList() override fun startObserving( onEach: (T) -> Unit, onError: (Throwable) -> Unit, onComplete: () -> Unit, ) { observeJobs += delegate .onEach(onEach) .onCompletion { onComplete() } .catch { onError(it) } .launchIn(coroutineScope) } override fun stopObserving() { observeJobs.forEach { it.cancel() } } }

The constructor must be internal because it requires the CoroutineScope type, which is not exported. This means the FlowObserver constructor can only be used in Kotlin/JS code.

The StateFlow type represents an observable source of values that always has a value. A value that wraps it should provide the value property to access the current state, a startObserving method to observe state changes, and a stopObserving method to stop all observers. Since StateFlow never completes, it doesn’t call the onComplete or onError methods.

@JsExport interface StateFlowObserver<T> : FlowObserver<T> { val value: T } fun <T> StateFlowObserver( delegate: StateFlow<T>, coroutineScope: CoroutineScope ): StateFlowObserver<T> = StateFlowObserverImpl(delegate, coroutineScope) class StateFlowObserverImpl<T>( private val delegate: StateFlow<T>, private val coroutineScope: CoroutineScope ) : StateFlowObserver<T> { private var jobs = mutableListOf<Job>() override val value: T get() = delegate.value override fun startObserving( onEach: (T) -> Unit, onError: (Throwable) -> Unit = {}, onComplete: () -> Unit = {}, ) { jobs += delegate .onEach(onEach) .launchIn(coroutineScope) } override fun stopObserving() { jobs.forEach { it.cancel() } jobs.clear() } }

Elements that are exposed in UserListViewModel, like List<User> or Throwable?, might not be understood by JavaScript, so we also need to create wrappers for them. For Flow this is a simple task as we can map its values using the map method. For StateFlow, we need to create a wrapper that maps objects.

fun <T, R> StateFlowObserver<T>.map( transformation: (T) -> R ): StateFlowObserver<R> = object : StateFlowObserver<R> { override val value: R get() = transformation(this@map.value) override fun startObserving( onEach: (T) -> Unit, onError: (Throwable) -> Unit = {}, onComplete: () -> Unit = {}, ) { this@map.observe { onEach(transformation(it)) } } override fun stopObserving() { this@map.stopObserving() } }

Now we can define the UserListViewModel class that can be used in JavaScript or TypeScript:

@JsExport("UserListViewModel") class UserListViewModelJs internal constructor( userRepository: UserRepository ) : ViewModelJs() { val delegate = UserListViewModel(userRepository) val userList: StateFlow<List<User>> = StateFlowObserver( delegate.usersList, viewModelScope ).map { it.map { it.asJsUser() }.toTypedArray() } val error: Flow<Throwable?> = FlowObserver( delegate.error.map { it?.asJsError() }, viewModelScope ) fun loadUsers() { delegate.loadUsers() } }

This is an example React hook that can simplify observing flow state:

export function useFlowState<T>(
    property: FlowObserver<T>,
): T | undefined {
    const [state, setState] = useState<T>()
    useEffect(() => {
        property.startObserving((value: T)=>setState(value))
        return () => property.stopObserving()
    }, [property])
    return state
}

// Usage
const SomeView = ({app}: { app: App }) => {
    const viewModel = useMemo(() => {
        app.createUserListViewModel()
    }, [])
    const userList = useStateFlowState(viewModel.userList)
    const error = useFlowState(viewModel.error)
    // ...
}

All these wrappers add a lot of boilerplate code, so I’m still hoping for a good KSP library or compiler plugin to generate them automatically. These wrappers are also not very efficient because they introduce additional objects, so it’s debatable whether making a common JavaScript module available is worth it for a specific application. I would say that if common parts are heavy in logic, then it should be worth the effort; on the other hand, if common parts are not logic-heavy, it might be better to duplicate them in JavaScript.

Adding npm dependencies

Adding Kotlin/JS dependencies to a Kotlin/JS project is easy: you just define them in the dependencies of this target. However, adding npm dependencies is a bit more demanding: you should also add them to the dependency list in build.gradle.kts, but you need to wrap such dependencies using the npm function.

// build.gradle.kts
kotlin {
 // ...
  
 sourceSets {
   // ...
   val jsMain by getting {
     dependencies {
       implementation(npm("@js-joda/timezone", "2.18.0"))
       implementation(npm("@oneidentity/zstd-js", "1.0.3"))
       implementation(npm("base-x", "4.0.0"))
     }
   }
   // ...
 }
}

Kotlin's dependencies have Kotlin types and can be used in Kotlin directly. JavaScript elements are not visible in Kotlin because they don’t have Kotlin types, so you need to define them in Kotlin if you want to use them. For that, define an external object, functions, classes, and interfaces. If these classes need to be imported from a library, you need to use the @JsModule annotation in front of the object representing the whole dependency. Here is an example of such definitions for the @oneidentity/zstd-js and base-x libraries:

@JsModule("@oneidentity/zstd-js") external object zstd { fun ZstdInit(): Promise<ZstdCodec> object ZstdCodec { val ZstdSimple: ZstdSimple val ZstdStream: ZstdStream } class ZstdSimple { fun decompress(input: Uint8Array): Uint8Array } class ZstdStream { fun decompress(input: Uint8Array): Uint8Array } } @JsModule("base-x") external fun base(alphabet: String): BaseConverter external interface BaseConverter { fun encode(data: Uint8Array): String fun decode(data: String): Uint8Array }

There is a library called Dukat6 that generates Kotlin declarations based on TypeScript definition files.

The Kotlin/JS Gradle plugin includes a dead code elimination tool that reduces the size of the resulting JavaScript code by removing unused properties, functions, and classes, including those from external libraries.

Frameworks and libraries for Kotlin/JS

Most of the problems I’ve described in this book result from interoperability between Kotlin and JavaScript, but most of them disappear if you limit or eliminate this interoperability. Instead of using an npm package, you can nearly always find a Kotlin/JS or multiplatform library that is easier to use and does not need any additional wrappers or special kinds of dependency declaration. I recommend starting your library search from the Kotlin Wrappers library, created by JetBrains. It contains an astonishing number of wrappers for different browser APIs and a variety of popular JavaScript libraries.

Instead of exporting elements so they can be used in JavaScript, you can write your client in pure Kotlin as well. For instance, the Kotlin Wrappers library offers methods for DOM manipulation or defining views using HTML DSL, or React Kotlin can define complete React applications using only Kotlin. There are also frameworks designed to be used in Kotlin/JS, like KVision, and JetPack Compose can be used to write websites.

JavaScript and Kotlin/JS limitations

When you consider targeting JavaScript, you should also consider its general limitations. JavaScript is an essentially different platform than JVM, therefore it has different types, different memory management, and different threading models. JavaScript runs on a single thread, and it is impossible to run blocking operations on JavaScript7. This means that you cannot use Dispatchers.IO in Kotlin/JS code.

You should also consider browser limitations. In one project, we established a WebSocket connection with a specific header in the handshake in our JVM client. However, it turned out that it’s not possible to do this in browsers due to their limitations. It’s a similar case with cookie headers: on JVM programs, this header can be set to whatever you want, but in browsers, you can only send actual cookies that are set for a specific domain.

The web is an extremely powerful platform and you would likely be surprised by web browsers’ capabilities, which include using databases, offline mode, background sync, shared workers and much more. However, the web also has its limits, so if you write a common module that should be used in a browser, you’d better know these limitations in advance.

On the other hand, it is also worth mentioning that Kotlin is much more limited than TypeScript when it comes to type systems: we cannot express type literals, union or intersection types, and much more. TypeScript is extremely expressive, and JavaScript, as a dynamic language, allows much more to be done with its objects than JVM, therefore Kotlin is more limited in the area of TypeScript API design.

// Example type that cannot be expressed in Kotlin
type ProcessResult = number | "success” | Error

Summary

As you can see, exposing Kotlin code to JavaScript is not that simple, but it’s possible, and it makes a lot of sense in some cases. In my AnkiMarkdown and SudokuSolver libraries, it was practically effortless because these libraries are heavy in logic and use very few platform-specific features. In the case of reusing the common logic of a complex application, it is much harder. We need additional wrappers, and we need to adjust our code to the limitations of JavaScript. I think it’s still worth it, but before you make a similar decision yourself, you should first consider all the pros and cons.

0:

The current implementation of Kotlin/JS compiles Kotlin code to ES5 or ES6.

1:

Link to AnkiMarkdown repository: github.com/MarcinMoskala/AnkiMarkdown

2:

Link to the repository of this project: github.com/MarcinMoskala/sudoku-generator-solver

3:

Link to this plugin repository: github.com/mpetuska/npm-publish

4:

By name mangling, I mean that these names are replaced with some random characters in JS.

5:

There are some discussions about making types like List, Map, Set and Long exportable to JavaScript. I truly support this with all my heart because interoperating between Kotlin and JavaScript/TypeScript seems to be the biggest pain at the moment.

6:

Dukat can be found at github.com/Kotlin/dukat

7:

It is possible to start processes in other threads in browsers that use Web Workers, and there is a KEEP (a document evaluated as part of the Kotlin Evolution and Enhancement Process) that proposes introducing support for a worker block that behaves similarly to the thread block on JVM.