How to create an "options object" (Object with many optional properties) in Kotlin compiling to JavaScript?

308 views Asked by At

Background: Options-objects in JavaScript

A common concept in JavaScript is something like this:

function MyLibrary(options) {
    if (options.optionA) { /* ... */ }
    if (options.optionB) { /* ... */ }
    if (options.flagC) { /* ... */ }
}

new MyLibrary({optionA: "foo", flagC: true})

So basically we have an "options"-object that contains many properties which are all optional.

The question: How to interact with that from Kotlin?

In Kotlin we would probably rather use named parameters with default values. However I would like to interact with an existing JavaScript-Library, which uses the concept described above. How can I describe and use that library from Kotlin? Of course it should be as type safe as possible.

What I have tried so far

Here is what I came up with:

interface Options {
    var optionA: String
    var optionB: Int
    var flagC: Boolean
}

external class MyLibrary(options: Options)

fun myFunWithApply() {
    MyLibrary(Any().unsafeCast<Options>().apply {
        optionA = "foo"
        flagC = true
    })
}

That does work, it also gives code completion for the options which is great, while it is not as short as the original JavaScript it feel OK considering its length. However... Are there better ways? Is there a way to get along without unsafeCast?

2

There are 2 answers

0
Davide Cannizzo On

Is there a way to get along without unsafeCast?

Yes, sure.

private external interface IOptions {
    val optionA: String
    val optionB: Int
    val flagC: Boolean
}

class Options(
    override val optionA: String,
    override val optionB: Int,
    override val flagC: Boolean
) : IOptions

external class MyLibrary(options: Options)

Usage:

fun foo() {
    MyLibrary(Options(
        optionA = "foo",
        flagC = true
    ))
}

The IOptions external interface is needed to prevent Options member names from being mangled. The Options class cannot be external on itself because there is no such class/constructor defined in JS.

The caveat is that the generated JS code is going to be slightly less efficient — there will be a whole additional class (Options) and a constructor invocation with parameters being assigned to properties, instead of just a JS object literal. However, once the code being involved gets jitted, it will probably make no difference.

0
Davide Cannizzo On

However... Are there better ways? Is there a way to get along without unsafeCast?

You might want to consider to keep using unsafeCast, but hiding it.

external interface Options {
    var optionA: String
    var optionB: Int
    var flagC: Boolean
}

external class MyLibrary(options: Options)

@OptIn(ExperimentalContracts::class)
inline fun Options(
    block: Options.() -> Unit
): Options {
    
    contract {
        callsInPlace(block, kind = InvocationKind.EXACTLY_ONCE)
    }
    
    return Any().unsafeCast<Options>().apply(block)
}

Usage:

fun myFunWithApply() {
    MyLibrary(Options {
        optionA = "foo"
        flagC = true
    })
}

This should translate to more efficient JS code, compared to my other answer. Not that it would generally matter, anyway.