Kotlin and TypeScript Integration

This article provides documentation for the Kotlin-TypeScript integration Gradle plugin – net.akehurst.kotlin.kt2ts.

Contact us

 

The plugin is published in the Gradle plugin registry, and the source code is published on GitHub.

An example project that illustrates the use of the plugin is also provided on GitHub.

The plugin covers two major issues that occur when trying to integrate Kotlin and TypeScript code.

  1. Generating TypeScript declaration (*.d.ts) files that correspond to the Kotlin generated JavaScript.
  2. Including the Kotlin-generated JavaScript into a Node.js based build.

The plugin is designed for use alongside the Kotlin multiplatform plugin. In order to generate the TypeScript declarations, the plugin makes use of the modules generated for a JVM target.

Simple usage

The plugin is added in the usual way as shown below:

plugins {
  id("net.akehurst.kotlin.kt2ts") version("1.4.0")
}

It can be configured using the kt2ts DSL extension, as described below.

Generating TypeScript declarations

To add TypeScript declarations and a package.json to your own modules, add the following:

kt2ts {
  classPatterns.set(listOf(
    "com.example.my.module.common.*"
  ))
}

This will create two tasks:

  • generatePackageJsonWithTypes
  • generateTypescriptDefinitionFile

and it will cause the jsJar task to have a dependency on them.

These tasks generate a TypeScript declaration file that contains declarations for everything contained in the com.example.my.module.common package. You can also specify classes individually if you do not want to generate TypeScript for everything in a package.

The resulting jsJar file that is built will contain the TypeScript declarations file, a package.json file, and the JavaScript that is generated by Kotlin.

A minimal package.json file is generated, like the following:

{
  "name": "com.example-my-module-common"
  "version": "1.0.0-SNAPSHOT",
  "main": "./com.example-my-module-common.js",
  "types": "./com.example-my-module-common.d.ts"
}

Node.js based build

This section describes the integration with an Angular build. However, one could potentially use the plugin with any TypeScript-based frontend.

You use the plugin to assist with building your Node.js-based frontend. To do this, it is necessary to define the location of the Angular source code directory.

kt2ts {
  nodeSrcDirectory.set(
    project.layout.projectDirectory.dir("src/angular")
  )
}

If you set the nodeSrcDirectory property then a number of extra tasks are added to the build. These new tasks do the following things:

  • kotlinNodeJsSetup, kotlinYarnSetup: Install Node.js and Yarn – privately, as part of the Gradle infrastructure. These tasks are provided by Jetbrains’ Kotlin plugin.
  • yarnInstall: Execute yarn install, which downloads and populates the node_modules directory for the Angular code.
  • unpackKotlinJs: Unpack the Kotlin JavaScript dependencies into the node_modules directory.
  • addKotlinStdlibDeclarations: Add a TypeScript declarations file for some of the commonly used types from the Kotlin stdlib.
  • ngBuild: Execute ng build, which builds the Angular code.

The tasks are all appropriately linked with dependencies. You need only to execute the following command:

gradle build

Adding Kotlin modules to the build

Of course, we do not want to build the Angular application in isolation. We want to add Kotlin modules that are to be used in the Angular application.

The plugin adds a dependency configuration, named nodeKotlin by default, that should be used to add dependencies to Kotlin multiplatform modules.

For example, assuming you have:

  • a module named my-module-common, which belongs to the group com.example,
  • used the kt2ts plugin to generate a TypeScript declarations file for that module.

Then we can add the module to the Angular build using the nodeKotlin dependency configuration:

dependencies {
  nodeKotlin("com.example:my-module-common:1.0.0")
}

This module will be unpacked in the Angular node_modules directory and can then be used in the Angular-TypeScript code as follows:

import * as mmc_js from 'com.example-my-module-common';
import mmc = mmc_js.com.example.my.module.common;
...
let x = new mmc.MyCommonClass();

The second import is not essential, it simply provides an alias to the content of the imported package. The Kotlin-generated JavaScript is namespaced according to the original Kotlin packages.

Advanced configuration

There are a number of additional configuration options that add extra features and assistance.

Generating TypeScript declarations

  • jvmTargetName [jvm]: the name of the Kotlin JVM target
  • jsTargetName [js]: the name of the Kotlin JS target
  • tsdOutputDirectory [tmp/jsJar/ts]: the (temporary) directory into which the package.json and *.d.ts file will be generated
  • declarationsFile [${project.group}-${project.name}.d.ts]: the name of the generated TypeScript declarations file
  • typeMapping: map of (fully-qualified) Kotlin type names with their TypeScript equivalents. The default includes the standard mappings used by the Kotlin to Javascript compiler. (See below for the full list).
  • moduleNameMap: map that defines a mapping from "{group}:{name}" strings of modules to the name of the respective JavaScript module. Unfortunately, this mapping is (currently) unpredictable and thus must be manually specified, unless the JavaScript module is named "{group}-{name}". The default includes mappings for commonly used modules from org.jetbrains.kotlinx:kotlinx-coroutines and io.ktor. (See below for the full list.)
  • includeOnly: list of "{group}:{name}" strings of modules that are used to restrict the classpath that is used for generating TypeScript declarations. Default is empty, which means no restrictions, if localOnly is set to false. Not used when localOnly is set to true.
  • localOnly [true]: restrict the classpath used for generating TypeScript declarations to be only the classes in the current (local) module.

Node.js based build

  • nodeOutDirectory [project.layout.buildDirectory.dir("angular")]: the directory into which the built code is placed
  • nodeBuildCommand: the node-specific build command and arguments, e.g.,
    listOf("ng", "build", "--prod", "--outputPath=${ngOutDir.get()}/dist")
  • nodeConfigurationName [nodeKotlin]: the name of the dependency configuration used to include Kotlin modules into the Node.js based build
  • nodeModulesDirectory [${nodeSrcDirectory}/node_modules]: the name of the node_modules directory
  • kotlinStdlibJsDirectory [${nodeModulesDirectory}/kotlin]: the name of the directory in which the JavaScript Kotlin stdlib can be found
  • excludeModules: list of "{group}:{name}" strings of modules that should not be unpacked. The default includes the Kotlin stdlib and reflect modules, because it is expected that these will be provided by the JavaScript-specific Kotlin module that is included via the Node.js package.json file.

TypeScript declarations for third-party modules

Often you want to include, into your Node.js based build, third-party modules for which there is no TypeScript declaration file. For example, org.jetbrains.kotlinx:kotlinx-coroutines-core or com.soywiz.korlibs.klock:klock (a very useful Kotlin multiplatform date-time library).

For these libraries, we do not have control of the source, so we cannot generate the TypeScript declarations as part of the {module}-js.jar. However, we can generate the declarations as part of the “unpacking” process, i.e., we can generate the declarations after the unpackKotlinJs task.

There is another configuration option generateThirdPartyModules that is part of the plugin’s Gradle configuration DSL. This can be used to configure TypeScript declaration generation for modules after they are unpacked.

kt2ts {
  ...
  generateThirdPartyModules {
    register("${group_klock}:klock:${version_klock}") {
      includeOnly.set(listOf("com.soywiz.korlibs.klock:klock-jvm"))
      moduleGroup.set("klock-root")
      moduleName.set("klock")
      tgtName.set("klock-root-klock")
      classPatterns.set(listOf(
        "com.soywiz.klock.DateTime",
        "com.soywiz.klock.DayOfWeek",
        "com.soywiz.klock.DateTimeTz",
        "com.soywiz.klock.TimezoneOffset",
        "com.soywiz.klock.Month",
        "com.soywiz.klock.Time",
        "com.soywiz.klock.Year",
        "com.soywiz.klock.YearMonth",
        "com.soywiz.klock.TimeSpan",
        "com.soywiz.klock.DateTimeSpan",
        "com.soywiz.klock.MonthSpan",
        "com.soywiz.klock.DateFormat"
      ))
    }
  }
}

Dynamic imports

Sometimes we have the requirement to access Kotlin Javascript classes via reflection. Unfortunately, most of the Javascript support for Kotlin reflection is not yet implemented. There is a small Kotlin multiplatform library that can be used to augment Kotlin reflection from Javascript. It is not complete either, but it adds a few things that are currently unavailable in the Kotlin standard reflection library.

This is used, for example, as part of a reflection-based JSON serialisation library, see the example project for an indication of how it is used.

In order to use the reflection library, it is necessary to “register” (in Kotlin code) the Kotlin modules that you wish to reflect over, i.e.,

import net.akehurst.kotlinx.reflect.ModuleRegistry
...
ModuleRegistry.register("com.example:my-module-common")

Correspondingly, in order to make these modules available in a “webpacked” Node.js application, it is necessary to generate some special code as part of a dynamic require function.

There is a specific configuration option, dynamicImport, for doing this as part of the plugin’s Gradle configuration DSL, e.g.,

kt2ts {
  ...
  dynamicImport.set(listOf(
    "com.example:my-module-common"
  ))
}

Appendix

Default typeMapping

mapOf(
  "kotlin.reflect.KClass" to "any",
  "kotlin.Unit" to "void",
  "kotlin.Any" to "any",
  "kotlin.Array" to "Array",
  "kotlin.CharSequence" to "string",
  "kotlin.Char" to "number",
  "kotlin.String" to "string",
  "kotlin.Number" to "number",
  "kotlin.Byte" to "number",
  "kotlin.Short" to "number",
  "kotlin.Int" to "number",
  "kotlin.Float" to "number",
  "kotlin.Double" to "number",
  "kotlin.Boolean" to "boolean",
  "kotlin.Throwable" to "Error",
  "kotlin.Exception" to "Error",
  "kotlin.RuntimeException" to "Error",
  "java.lang.Exception" to "Error",
  "java.lang.RuntimeException" to "Error"
)

Default moduleNameMap

mapOf(
  "org.jetbrains.kotlinx:kotlinx-coroutines-core-js" to "kotlinx-coroutines-core",
  "org.jetbrains.kotlinx:kotlinx-coroutines-core" to "kotlinx-coroutines-core",
  "org.jetbrains.kotlinx:kotlinx-coroutines-core-common" to "kotlinx-coroutines-core-common",
  "org.jetbrains.kotlinx:kotlinx-coroutines-io-js" to "kotlinx-io-kotlinx-coroutines-io",
  "org.jetbrains.kotlinx:kotlinx-coroutines-io" to "kotlinx-io-kotlinx-coroutines-io",
  "org.jetbrains.kotlinx:kotlinx-io-js" to "kotlinx-io",
  "org.jetbrains.kotlinx:kotlinx-io" to "kotlinx-io",
  "org.jetbrains.kotlinx:atomicfu-common" to "kotlinx-atomicfu",
  "org.jetbrains.kotlinx:atomicfu-js" to "kotlinx-atomicfu",
  "org.jetbrains.kotlinx:atomicfu" to "kotlinx-atomicfu",
  "io.ktor:ktor-http-cio-js" to "ktor-ktor-http-cio",
  "io.ktor:ktor-http-cio" to "ktor-ktor-http-cio",
  "io.ktor:ktor-client-core-js" to "ktor-ktor-client-core",
  "io.ktor:ktor-client-core" to "ktor-ktor-client-core",
  "io.ktor:ktor-client-websockets-js" to "ktor-ktor-client-websockets",
  "io.ktor:ktor-client-websockets" to "ktor-ktor-client-websockets",
  "io.ktor:ktor-http-js" to "ktor-ktor-http",
  "io.ktor:ktor-http" to "ktor-ktor-http",
  "io.ktor:ktor-utils-js" to "ktor-ktor-utils",
  "io.ktor:ktor-utils" to "ktor-ktor-utils"
)

Example mapping Kotlin to TypeScript

Kotlin

package com.example.my.module.common

data class MyCommonClass(name: String) {
  var list = mutableListOf<Pair<Int,Boolean>>()
}

Typescript Declaration

import * as $kotlin from 'kotlin';

declare namespace com.example.my.module.common {
  class MyCommonClass {
    constructor(name: string);
    list: $kotlin.collections.List<$kotlin.Pair<number,boolean>>
  }
}