Feedback on our migration from Typescript to Kotlin


#1

Hi there,

I work at Clue (helloclue.com), and we are currently migrating some library from Typescript to Kotlin. This library is an NPM module that runs in multiple environments, namely in an iOS App, an Android App and our Node backend. We started looking for an alternative technology as there seems to be no satisfying way to run Javascript code on the Android clients. We picked Kotlin as it is native on Android and compiles down to Javascript too, allowing us to preserve backwards-compatibility for existing clients.

In our configuration, we have a multi-project Gradle build:

  • a “shared” project with the Kotlin sources and the related unit tests. It has a specific “shared” sourceSet that is standard-library-agnostic and that can be imported in the JS build.
  • a “js” project that compiles the “shared” sourceSet of the “shared” project to Javascript, and assembles an NPM package
  • a “typescript” project that contains all of the legacy sources and that depends on the build of the “js” project.

Migration pain points

Kotlin seems to deliver its promises, the migration is so far a success. Congratulations for the solidity of this early version :slight_smile:

I still want to share some pain points. Indeed, some issues I encountered might already have a solution. The current tooling around Kotlin-JS seems to be targeting Web development. As we’re instead building and publishing a Node package, I encountered a number of specific needs.

Typescript type definition

We need to provide Typescript type definitions along with the JS sources. At the moment, we maintain them manually, but that’s a lot of hassle for something that actually looks easy to automate.

package.json

We need to distribute our NPM package with a package.json file describing the right dependencies (at least kotlin, in the correct version). kotlin-frontend-plugin can generate such a package.json file, but only if at least one NPM dependency is specified. Also, most of the fields receive dummy/invalid values that can’t be configured. So it doesn’t seem to be the right tool for our needs. For now, we use a simple groovy template for this purpose.

The fact that the latest kotlin library on npmjs is in version 1.1.0, and therefore different from the version of kotlin we specify in our build (1.1.2-2), doesn’t help.

Companion object

Being able to expose the functions of the companion object as static functions on the JS classes would be really cool. This would help us preserve backwards-compatibility as we migrate our code to Typescript.

Java packages

It is a best practice in the Java world to organise sources in packages with extensive names (com.helloclue.projectname.*), but this is not the case in Javascript as imports work very differently. I’ve been looking for a way to get rid of that namespace for the JS build.

Mixing Kotlin and Javascript sources

To solve the issue mentioned above, it could have been useful to be able to have a Javascript sourceSet taken in account by the kotlin2js Gradle plugin. This feature doesn’t seem to be available - at least it’s not documented.

Incompatible standard libraries

Math is not visible with the Kotlin JS standard library. Our workaround was to create a typealias from Math to kotlin.js.Math in our JS build.
I haven’t searched very far here, but it seems like Dates, number formats… are not necessarily behaving in the same way in the Java build and the JS build. I wonder if this is on purpose (APIs seen as too platform-specific) or if this will be improved in future versions.

The annotation @JsName is not available in the “Java” standard library, so we can’t use directly the annotation on our shared code. Our workaround was here also to create a typealias.

@JsName required on all functions

@JsName is supposed to be useful in case of function overload. However, it seems like the compiler will always add a suffix to the function name if the annotation is omitted. As a result, we have to add the annotation on each and every function of our codebase. This seems to be a bug.

Conclusion

We’re moving ahead with Kotlin, as it is the best solution to our use-case. None of the elements mentioned above is blocking. Thanks to all of the Kotlin team for the great work :thumbsup:


#2

To solve the issue mentioned above, it could have been useful to be able to have a Javascript sourceSet taken in account by the kotlin2js Gradle plugin

What do you mean? How should gradle plugin process JS sources?

@JsName is supposed to be useful in case of function overload. However, it seems like the compiler will always add a suffix to the function name if the annotation is omitted. As a result, we have to add the annotation on each and every function of our codebase. This seems to be a bug.

It’s not a bug. Imagine, you have fun foo(x: Int), which gets translated to foo function in JavaScript. Then, you extend your API and add fun foo(x: String) to the same class. To distinguish between these function, Kotlin compiler should mangle both names. This means that old code that relied on the fact that foo(x: Int) == foo function in JavaScript becomes broken and you have to recompile it. Ok, it’s not an issue for you, but imagine you ditribute your class as a part of a library, of which another library called bar depends, and then it’s used as a dependency. Imagine that developer uses bar in his project (and therefore your library as a transitive dependency). The developer should download and recompile bar sources just to upgrade to newer version of your library!

You don’t have to put @JsName on each and every function. Just put on functions that should be exposed to external code. Also, we were going to invent annotation that controls mangling on whole classes and (possibly) packages.


#3

Hi Alexey,

Thanks for the answer :slight_smile:

What do you mean? How should gradle plugin process JS sources?

I don’t expect the Kotlin compiler to make anything of the JS sources. However, being able to have Kotlin and Javascript/Typescript sources in a single project, bundled by Gradle build into a single NPM package, could be useful. I imagine the Javascript/Typescript code could reference the Kotlin code, but not the other way around.

At the moment, having a dependency between distinct projects seems to be the best way to achieve that goal. It’s just not straightforward at the moment. We’re using a combination of gradle-node-plugin, Yerna and Webpack to manage dependencies between project and build the final binaries.

Some good example projects might just do. Maybe some exist already?

You don’t have to put @JsName on each and every function. Just put on functions that should be exposed to external code.

As we’re currently porting file-by-file a Typescript codebase to Kotlin, we really do have to annotate each function (despite the fact that there is no overloading in our entire codebase).

The ideal compiler in my case would:

  • require @JsName in case of function overload
  • preserve original function names when there is no overload

Having this behaviour behind a compiler flag would be totally OK.

Also, we were going to invent annotation that controls mangling on whole classes and (possibly) packages.

This sounds like a great addition to the existing toolset. Looking forward to try these :thumbsup:


#4

One problem with @JsName (and for that matter @JvmName) is that the JS compiler doesn’t know about @JsName and reverse. It would be helpful if these were recognised (but completely ignored) so that the presence of the annotations does not cause compilation failures.


#5

@fbecart Thanks for this detailed description of you using Kotlin to share code between android and iOS. I actually tried to take a similar approach, but ultimately gave up when I couldn’t find a way to debug the kotlin source sets on iOS. I found a way to have limited debugging of the generated js code, but my team decided that wasn’t good enough. Did you guys find a solution for the debugging issues that stopped us?

Thanks,
Scott


#6

@spierce7 We don’t use debugging at the moment. The existing Typescript library has great code coverage and is battle-tested in production, so the need for it is limited.

The Typescript library and the iOS App are maintained by a different set of people. Usually, in case of issues, the iOS team provides a dump of the “runtime context”, which allows us to reproduce the issue easily directly in the Typescript project, and run in debug mode if necessary. So far this has worked for us :slight_smile:


#7

@pdvrieze Right. At the moment, if a single codebase targets multiple platform, you can’t simply use the annotations :confused:


#8

Currently, we are working on improving debugging experience, and I hope we’ll be able to deliver multiple bugfixes in JS source map generation in minor update for 1.1 (not in 1.1.3, but somewhere in >= 1.1.4).


#9

Regarding the point “Typescript type definition”:

You could take a look at https://github.com/dzuvic/jtsgen

It is an annotation processor that provides TypeScript Type Definition Files from Java/Kotlin sources.


#10

Thanks for the tip. I’ll definitely take a look at this library :slight_smile: This could remove a lot of complexity in my project.