Within the javascript ecosystem. There is a push for type safety. This can be see with things like ReasonML, TypeScript and Flow. There may be more, but those are the ones I know of.

Type script has a concept of declaration files. The central repository for type declarations can be seen here. So we have this rich ecosystem of type definitions. Now we have Kotlin a type safe language that compiles down to java script. Now only if we could combine these two?

That is where ts2kt comes in. This script will convert typescript definitions to kotlin files. Now it's not all rosey. You can be in for a rabbit hole, or something super simple. The project I've been working with is material-ui.

Focusing specifically on that example. In React these material components, have the concept of props. For those not familiar with react. The props interface is like a set of arguments that can be accepted.

So why is this beneficial? With a type system if I pass a prop that is expecting a string, but I pass a boolean. Java script will probably run fine. But it may result in some erroneous bugs. But with KotlinC, or any of the other type safe languages. It would catch that type mismatch. So this is a broader argument for type safety on your frontend. But with later pieces I write I will provide further arguments in front of Kotlin.

So how do we convert types?

Install ts2kt if not present.

sudo npm install -g ts2kt

Running ts2kt

ts2kt -d src/main/kotlin 
build/node_modules/@material-ui/core/Snackbar/Snackbar.d.ts 130 ↵
ts2kt version: 0.0.20
Converting build/node_modules/@material-ui/core/Snackbar/Snackbar.d.ts
ts2kt: "ImportDeclaration" kind unsupported yet here! (build/node_modules/@material-ui/core/Snackbar/Snackbar.d.ts:1:1 to 1:32)
ts2kt: "ImportDeclaration" kind unsupported yet here! (build/node_modules/@material-ui/core/Snackbar/Snackbar.d.ts:1:32 to 2:36)
ts2kt: "ImportDeclaration" kind unsupported yet here! (build/node_modules/@material-ui/core/Snackbar/Snackbar.d.ts:2:36 to 3:59)
ts2kt: "ImportDeclaration" kind unsupported yet here! (build/node_modules/@material-ui/core/Snackbar/Snackbar.d.ts:3:59 to 4:85)
ts2kt: "ExpressionStatement" kind unsupported yet here! (build/node_modules/@material-ui/core/Snackbar/Snackbar.d.ts:28:40 to 28:49)
ts2kt: "EmptyStatement" kind unsupported yet here! (build/node_modules/@material-ui/core/Snackbar/Snackbar.d.ts:28:50 to 28:51)
ts2kt: "ExpressionStatement" kind unsupported yet here! (build/node_modules/@material-ui/core/Snackbar/Snackbar.d.ts:28:51 to 29:37)
Save declarations:
	src/main/kotlin/Snackbar.kt
  • ts2kt The core command
  • -d src/main/kotlin The destination of the converted files.
  • build/node_modules/@material-ui/core/Snackbar/Snackbar.d.ts The typescript file we wish to convert.

Onward Will Robinson

Okay so what did that spit out?

@file:Suppress("INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS", "EXTERNAL_DELEGATION", "NESTED_CLASS_IN_EXTERNAL_INTERFACE")

import kotlin.js.*
import kotlin.js.Json
import org.khronos.webgl.*
import org.w3c.dom.*
import org.w3c.dom.events.*
import org.w3c.dom.parsing.*
import org.w3c.dom.svg.*
import org.w3c.dom.url.*
import org.w3c.fetch.*
import org.w3c.files.*
import org.w3c.notifications.*
import org.w3c.performance.*
import org.w3c.workers.*
import org.w3c.xhr.*

external interface SnackBarOrigin {
    var horizontal: dynamic /* Number | String /* "left" */ | String /* "center" */ | String /* "right" */ */ get() = definedExternally; set(value) = definedExternally
    var vertical: dynamic /* Number | String /* "center" */ | String /* "top" */ | String /* "bottom" */ */ get() = definedExternally; set(value) = definedExternally
}
external interface SnackbarProps : StandardProps<React.HTMLAttributes<HTMLDivElement> /* React.HTMLAttributes<HTMLDivElement> & Partial<TransitionHandlerProps> */, SnackbarClassKey> {
    var action: dynamic /* React.ReactElement<Any> | Array<React.ReactElement<Any>> */ get() = definedExternally; set(value) = definedExternally
    var anchorOrigin: ``"build/node_modules/@material-ui/core/Snackbar/Snackbar".SnackBarOrigin``? get() = definedExternally; set(value) = definedExternally
    var autoHideDuration: Number? get() = definedExternally; set(value) = definedExternally
    var ContentProps: Partial<SnackbarContentProps>? get() = definedExternally; set(value) = definedExternally
    var disableWindowBlurListener: Boolean? get() = definedExternally; set(value) = definedExternally
    var message: React.ReactElement<Any>? get() = definedExternally; set(value) = definedExternally
    var onClose: ((event: React.SyntheticEvent<Any>, reason: String) -> Unit)? get() = definedExternally; set(value) = definedExternally
    var onMouseEnter: React.MouseEventHandler<Any>? get() = definedExternally; set(value) = definedExternally
    var onMouseLeave: React.MouseEventHandler<Any>? get() = definedExternally; set(value) = definedExternally
    var open: Boolean
    var resumeHideDuration: Number? get() = definedExternally; set(value) = definedExternally
    var TransitionComponent: React.ReactType? get() = definedExternally; set(value) = definedExternally
    var transitionDuration: Array<TransitionProps>? get() = definedExternally; set(value) = definedExternally
}
@JsName("default")
external var Snackbar: React.ComponentType<SnackbarProps> = definedExternally

This feels really dense. But let's break this down.

Firstly if there are api docs load them. In the case of material ui we have this. We have two seperate interfaces for the component. One is a companion interface, used some where in the props. The other is the actual props.

external interface SnackBarOrigin {
    var horizontal: dynamic /* Number | String /* "left" */ | String /* "center" */ | String /* "right" */ */ get() = definedExternally; set(value) = definedExternally
    var vertical: dynamic /* Number | String /* "center" */ | String /* "top" */ | String /* "bottom" */ */ get() = definedExternally; set(value) = definedExternally
}

Which we can see is being used here.

    var anchorOrigin: `"build/node_modules/@material-ui/core/Snackbar/Snackbar".SnackBarOrigin`? get() = definedExternally; set(value) = definedExternally

This can be shortened to the following.

    var anchorOrigin: SnackBarOrigin? get() = definedExternally; set(value) = definedExternally

So we switched the build reference directly to the actual interface. The rest is automatically propegated.

Looking at the API docs we see it's under anchorOrigin, and is an enum. Disclosure I've not worked out the best way to handle enums, so I default it to string for now.

Next up is what SnackBarProps extends.

StandardProps<React.HTMLAttributes<HTMLDivElement> /* React.HTMLAttributes<HTMLDivElement> & Partial<TransitionHandlerProps> */, SnackbarClassKey>

In some other components we may see it extended other material components. But when you see StandardProps and HtmlAttributes This can just be Rprops. An example in the material ui is you would see something like.

external interface SnackbarProps : StandardProps<ButtonProps> {

This means it is inheriting off of the ButtonProps. However we're working off standard props. So you can use the following. This is based of the standard react props.

external interface SnackbarProps : RProps {

Hello Rabbit Hole

There is one major gotcha in this.

var TransitionProps: TransitionProps? get() = definedExternally; set(value) = definedExternally

Why is this a gotcha, well convert it like so:

ts2kt -d src/main/kotlin build/node_modules/@material-ui/core/transitions/transition.d.ts 

Which gives us:

external interface TransitionProps : TransitionActions, Any? {
    var style: CSSProperties? get() = definedExternally; set(value) = definedExternally
}

So CSSProperties can't be found. Well it's not Kotlin bound yet that I can find. But it's totally there.

This is one of the big points I've found. With external libraries and javascript. You'll be spending a lot of time working on bindings and type mappings. Coalescing them into the Kotlin ecosystem. I find that it's worth it. But I think it's a hard sell to upper management.

Boiler Plate

Now let's actually convert this into something digestible :).

Set a Package

I've found react works best with a name space hierarchy. As an example we can use the namespaced react.materialui. So any of the components can be inherited from that name space. Below the @file:Supress line add a package statement, like:

package react.materialui

Import the Module

@JsModule("@material-ui/core/Button/Button")
external val ButtonImport: dynamic

Here we're telling to import the java script module at the given path. This is much like a bog standard ECMA import statement. We are then saying it is an external module not within kotlin. Then assigning it to the val of ButtonImport. Casting it as a dynamic case.

For my standards I name the JS module import as ${ClassName}Import.

Expose the Java Script Class

var Button: RClass<ButtonProps> = ButtonImport.default

So we're exposing an item named Button, which is of the type React Class. Which accepts a propr interface of Button Props. It is being pulled from the prior set module import. The full file looks like.

@file:Suppress("INTERFACE_WITH_SUPERCLASS", "OVERRIDING_FINAL_MEMBER", "RETURN_TYPE_MISMATCH_ON_OVERRIDE", "CONFLICTING_OVERLOADS", "EXTERNAL_DELEGATION", "NESTED_CLASS_IN_EXTERNAL_INTERFACE")
package react.materialui
import org.w3c.dom.events.Event
import react.RClass
import react.RProps

@JsModule("@material-ui/core/Button/Button")
external val ButtonImport: dynamic

external interface ButtonProps : RProps {
    var color: String? get() = definedExternally; set(value) = definedExternally
    var component: String? get() = definedExternally; set(value) = definedExternally
    var disabled: Boolean? get() = definedExternally; set(value) = definedExternally
    var disableFocusRipple: Boolean? get() = definedExternally; set(value) = definedExternally
    var disableRipple: Boolean? get() = definedExternally; set(value) = definedExternally
    var fullWidth: Boolean? get() = definedExternally; set(value) = definedExternally
    var href: String? get() = definedExternally; set(value) = definedExternally
    var mini: Boolean? get() = definedExternally; set(value) = definedExternally
    var size: dynamic /* String /* "small" */ | String /* "medium" */ | String /* "large" */ */ get() = definedExternally; set(value) = definedExternally
    var type: String? get() = definedExternally; set(value) = definedExternally
    var variant: dynamic /* String /* "raised" */ | String /* "fab" */ | String /* "flat" */ | String /* "outlined" */ */ get() = definedExternally; set(value) = definedExternally
    var onClick : (Event) -> Unit
}


var Button: RClass<ButtonProps> = ButtonImport.default

Calling It

import react.materialui.*

class Home : RComponent<RProps, RState>() {
    override fun RBuilder.render() {
        PageBase {
                Button {
                    attrs.color = "primary"
                    attrs.onClick = { _ -> window.open("https://gitlab.com/AnimusDesign/KotlinMultiPlatformSkeleton/issues/new") }
                    +"Open an Issue against Kotlin Multi Platform Skeleton"
                }
        }
    }
}

Here we're building a component called Home. It does not define any custom props or state. So we're using the standard react interfaces.

We're pulling in the defined Button object above. With typesafety. The props in the interface are assigned via:

attrs.color = "primary"

Or via a closure, like:

attrs {
   color = "primary"
}

By type safe I mean if we provided an attrs of a property not present in the interface. A compilation error would be thrown. If we provided a Boolean where a string is expected, type error, etc. I consider this a win, not a hinderance :).

Caveats

In the above sample, color is a discrimanted union or enum. I have not gotten to this, but the best option would be to cast that to an enum, and on the getter have it return the string representation.

External Reference

For an end to end project, including packaging into a jar. For consumption via gradle, see the following project. It's still very much under development but is an active start.