JS in Kotlin/JS
Tags: kotlin, kotlinjs, javascript
Published: 2021-12-30 18:14:39

Kotlin/JS brings the full awesomness of Kotlin language to the JS ecosystem, providing great standard library, typesafety and lots of modern features not found in vanilla JS. 

However one of the biggest strengths of the JS ecosystem is its massive collection of libraries ready for you to use. Kotlin/JS has full interop with JS code, however, just like TS, it demands external declarations to describe JS API surface. There are ways to shut Kotlin compiler up and proceed in a type-unsafe way (ehem, `dynamic` type), however that beats the whole point of Kotlin as a typesafe language.

Enter this article! Here we'll cover how Kotlin external declarations map to JS imports and how to write your own from scratch. Hopefully you'll learn some tips and tricks along the way.

## Basics
### JS Module Mapping
To make your Kotlin code play nice with JS code, Kotlin stdlib provides few compiler-targeted annotations usable in tandem with `external` keyword. Note that `external` keyword is only required at the top-level declarations and nested declarations are implied to be `external`. 
Consider the following example:
```kotlin
@JsModule("module-name")               // 1
@JsNonModule                           // 2
external val myExternalModule: dynamic // 3
```
1. Tells the compiler that this declaration maps to JS module `module-name`
2. Tells the compiler that this declaration can also work with UMD resolver. Not needed when using CommonJS.
3. Declares an `external` value with `dynamic` type. This is a reeference to external JS code we can now use from our Kotlin code! `dynamic` type is an escape hatch, basically telling the compiler that the shape of this value can be whatever (just like in vanilla JS). We'll look into how to make that type-safe later on.

### Entity Mapping
So far we've only seen a top-level `value` marked as external, however it does not stop there. Kotlin/JS supports `object`, `class`, `interface`, `fun` and even nested declarations for external scope modelling. Here's the recommended mapping between JS and Kotlin entities to use when writing your own declarations:
* [JS] fields and properties (declared with `get` and `set` keywords -> [Kotlin] `val` or mutable `var`
* [JS] functions and lambdas -> [Kotlin] `fun` member functions or lambda `val`
* [JS] `class` -> [Kotlin] `class`
* [JS] anonymous object shapes (`{}`) -> [Kotlin] `interface`

With the above suggestion in mind, here's how all these entities in JS translate to Kotlin:
```javascript
class MyJSClass {
  myField
  constructor(initField = "69") {
    this.myField = initField
  }
  function myMethod(arg1 = 420) {
    return arg1 + 1
  }
  get myProperty() {
    return this.myField
  }
  set myProperty(value) {
    this.myField = value
  }
  get myImmutableProperty() {
    return this.myField
  }
  
  myLambda = () => ({ result: 1, answer: "42" })
}
```
```kotlin
external class MyJSClass(initField: String = definedExternally) {
  var myField: String
  fun myMethod(arg1: Int = definedExternally): Int
  var myProperty: String
  val myImmutableProperty: String
  
  interface MyLambdaReturn {
    var result: Int
    var answer: String
  }
  val myLambda: () -> MyLambdaReturn
}
```
Note the special `definedExternally` value. It's a neat way to tell the compiler that an argument has a default value in JS without having to hard-code it in the Kotlin declarations as well. It can also be used to declare optional properties on external interfaces that you plan on constructing in Kotlin (to pass as arguments to other external entities). There's a slight limitation to this trick - only nullable types can have default implementations declared.
```kotlin
external interface MyJSType {
  val optionalImmutableValue: String?
    get() = definedExternally
  var optionalMutableValue: String?
    get() = definedExternally
    set(value) = definedExternally
}
val myJsTypeInstance: MyJSType = object: MyJSType {
  // Now we only need to override properties we want to set
  override val optionalImmutableValue: String? = "noice"
}
```

### Async entities
Async entities in JS are nothing different to regular entities when wrapping them to kotlin, however there are few things one needs to be aware of.
Firstly, `async` keyword can be ignored alltogether from Kotlin side as it's just JS syntactic sugar to unwrap Promise based APIs to callback APIs (quite similar to `suspend` in Kotlin). 
Secondly, there's currently no direct interop between JS `async` and Kotlin `suspend`. However you can still make them work nicely together with a few utilities from the coroutines runtime library. Let's jump straight to code to see how. As before, considder two files - external JS library file and our kotlin file. For simplicity we'll assume that external JS function is in the global scope.
```typescript
async function returnJSPromise(): Promise<string> {
  return Promise.resolve("Async hello from JS")
}
async function receiveJSPromise(promise: Promise<string>): Promise<void> {
  const ktResolvedValue = await promise
  console.log(ktResolvedValue)
}
```
```kotlin
external fun returnJSPromise(): Promise<String>

fun main() {
  // Promise APIs do not require async/suspend scope to use, just like in JS!
  val jsPromise: Promise<String> = returnJSPromise()
  
  // Resolving Promise values, however, does. Just like in JS!
  GlobalScope.launch {
    // `.await()` is an suspending extension function that allows one to await JS promises in coroutine scope
    val jsResolvedValue: String = jsPromise.await()
    println(jsResolvedValue)

    // `promise{}` is yet another coroutine builder that allows building JS Promise within a given coroutine scope
    val ktPromise = promise {
      delay(1000)
      "Async hello from KT"
    }
  }
}
```

## Declaring NPM packages
Most of the time you'll need to work with NPM packages, which comes with a single entry-point declared in the `package.json` and re-exports deeply nested moduled from a single module. 

To declare such packages in Kotlin, there are two strategies for you to use - `object` and `file`.

To showcase both, consider this JS module named `js-greeter` example and see how it can be declared in Kotlin:
```javascript
export const value = "69"
export const anonymousObjectValue = {
  name: "John"
}
export class JSClass {
  static function initialise() {}
  memberValue = 420
}
export function defaultHello() {
  return "Default Hi"
}
export const helloLambda = (name = "Joe") => (`Hello ${name}`)
export default defaultHello
```

### NPM Package Object
When declaring an object as a container for an external NPM package, that object takes a role of the entire module. When using this strategy, the file can contain a mix of both, external and regular Kotlin declarations.
```kotlin
@JsModule("js-greeter")
external object JSGreeter {
  val value: String

  object anonymousObjectValue {
    var name: String
  }

  class JSClass {
    companion object {
      fun initialise()
    }
    val memberValue: Number
  }

  fun defaultHello(): String

  fun helloLambda(name: String = definedExternally): String

  @JsName("default") // Overriding JS name mapping to `default` rather than `defaultExportedHello`
  fun defaultExportedHello(): String
}
```

### NPM Package File
When declaring a file as a container for an external NPM package, that file takes a role of the entire module and declarations inside that file match 1:1 to the JS module file. When using this strategy, the file can only contain external declarations and mixing of regular Kotlin and external declarations is not allowed. Finally, since all declarations are no longer nested inside `external object` and instead are top-level declarations, each of them must be marked as `external` individually.
```kotlin
@file:JsModule("js-greeter")

external val value: String

external object anonymousObjectValue {
  var name: String
}

external class JSClass {
  companion object {
    fun initialise()
  }
  val memberValue: Number
}

external fun defaultHello(): String

external fun helloLambda(name: String = definedExternally): String

@JsName("default") // Overriding JS name mapping to `default` rather than `defaultExportedHello`
external fun defaultExportedHello(): String
```

## Declaring Global JS API
Sometimes you might need to hook into some JS API that does not come from NPM but is provided by the runtime in the global scope. In such cases all you need is to declare the API shape anywhere in your project without any of the module annotations. Here's an example of how to get access to ES6 dynamic imports (note that the return `Promise` type comes from WEB API declarations provided in Kotlin standard library)
```kotlin
external fun import(module: String): Promise<dynamic>
```

## Declaring non-JS modules
JS development has evolved past JS-only projects and often uses various webpack loaders to "import" non-JS assets. This is possible in Kotlin/JS as well via the same strategies that we used to import JS modules. It's important to note that just like in JS, appropriate webpack loaders must be configured for such imports to work.

Here are some exotic JS import examples and their equivalents in Kotlin.
```javascript
import CSS from "my-library/dist/css/index.css"
import SCSS from "my-library/dist/scss/index.scss"
import JsonModule from "my-library/package.json"
```
```kotlin
@JsModule("my-library/dist/css/index.css")
external val CSS: dynamic

@JsModule("my-library/dist/scss/index.scss")
external val SCSS: dynamic

@JsModule("my-library/package.json")
external val JsonModule: dynamic
```

## Getting Rid of dynamic Type
While `dynamic` type is very convenient and useful in places where you want to tie-up external API declarations chain, it discards all type-safety that Kotlin provides. In most of the cases you should aim to declare the shape of the type via an `external interface` instead. While external interfaces can be nested inside your module declarations, it is not mandatory and they can live anywhere in your project because they're discarded during compilation and are not present at runtime.
```kotlin
@JsModule("my-library/package.json")
external val packageJson: dynamic

// === VS ===

external interface PackageJson {
  val name: String
  val private: Boolean
  val bundledDependencies: Array<String>
}

@JsModule("my-library/package.json")
external val typedPackageJson: PackageJson
```

They can also be used to reuse common traits between external declarations by making other external declarations (such as classes) implement such external interfaces.

## Summary
We've seen lots of options available to us when mapping Kotlin code to external JS code in order to maintain type safety and unlock a massive ocean of NPM libraries. Hopefully you found something useful in here. 

If I missed anything, let me know in the comments and I'll add it in to make this article as complete as possible. 

Happy coding!