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 thejs
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.
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.
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:
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:
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:
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
.
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.
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.publish
3 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 theLong
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:
JsName
annotation is used to change the name of an element in JavaScript. We often useJsName
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:
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.
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.
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.
Now we can define the UserListViewModel
class that can be used in JavaScript or TypeScript:
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:
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.
The current implementation of Kotlin/JS compiles Kotlin code to ES5 or ES6.
Link to AnkiMarkdown repository: github.com/MarcinMoskala/AnkiMarkdown
Link to the repository of this project: github.com/MarcinMoskala/sudoku-generator-solver
Link to this plugin repository: github.com/mpetuska/npm-publish
By name mangling, I mean that these names are replaced with some random characters in JS.
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.
Dukat can be found at github.com/Kotlin/dukat
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.