javascriptkotlinkotlin-jskotlin-js-interop

Instantiate Javascript classes that expect "new" keyword on KotlinJS


considering the following javascript code (partially taken from Apollo Server documentation), it creates an instance of ApolloServer and start it.


const {ApolloServer} = require('apollo-server')

const server = new ApolloServer({ ... });

server.listen().then(({ url }) => {
  console.log(`Server ready at ${url}`);
});

Now consider to replicate the same behaviour using KotlinJS. Firstly, Kotlin doesn't have the "new" keyword and calling ApolloServer() as expected, won't work but raise an error (TypeError: Class constructor ApolloServer cannot be invoked without 'new').

// We can banally represent part of the code above like:
external fun require(module: String): dynamic
val ApolloServer = require("apollo-server").ApolloServer

// ApolloServer is a js class

Declaring an external class like:

external open class ApolloServer() {
    open fun listen(vararg opts: Any): Promise<Any>
    operator fun invoke(): Any
}

and set it as ApolloServer type doesn't help.

How do we replicate "new ApolloServer()" call?


Solution

  • To solve this problem I found an interesting approach based on JsModule annotation. We need to create a Kotlin file that represent that javascript module we want to import, in my case "apollo-server".

    @file:JsModule("apollo-server")
    @file:JsNonModule
    package com.package
    
    import kotlin.js.Promise
    
    external interface ServerInfo {
        var address: String
        var family: String
        var url: String
        var subscriptionsUrl: String
        var port: dynamic /* Number | String */
            get() = definedExternally
            set(value) = definedExternally
        var subscriptionsPath: String
        var server: Any
    }
    
    external open class ApolloServer(config: Any? /* ApolloServerExpressConfig & `T$0` */) : Any {
        open var httpServer: Any
        open var cors: Any
        open var onHealthCheck: Any
        open var createServerInfo: Any
        open fun applyMiddleware()
        open fun listen(vararg opts: Any): Promise<ServerInfo>
        open fun stop(): Promise<Unit>
    }
    

    With the above code we are basically describing what we expect to find in the apollo-server module and how to map it into Kotlin.

    In our Kotlin main function we don't have to specify any require(...) but just use our ApolloServer class like:

        ApolloServer(null).listen().then {
           console.log(it)
        }
    

    Using this approach Kotlin would transpile it correctly, using the new keyword in javascript.

    Transpiled version extract:

      function main$lambda(it) {
        console.log(it);
        return Unit;
      }
      function main() {
        (new ApolloServer(null)).listen().then(main$lambda);
      }
    

    This code is just an example, ApolloServer won't be initialized without a proper configuration, this case, for example, contains a nullable configuration.