KotlinJS object literals generate mangled names, breaking hasOwnProperty


#1

This is less of a bug report and more calling out an inconvenient, breaking pattern, but here goes.

If I write a call like this:

anims.create(object : AnimationConfig {
    override var key: String? = "misa-left-walk"
    override var frames: Array<AnimationFrameConfig>? = anims.generateFrameNames("atlas", json("prefix" to "misa-left-walk", "start" to 0, "end" to 3, "zeroPad" to 3))
    override var frameRate: Int? = 10
    override var repeat: Int? = -1
})

where AnimationConfig is something like this:

external interface AnimationConfig {
    var key: String? get() = definedExternally; set(value) = definedExternally
    var frames: Array<AnimationFrameConfig>? get() = definedExternally; set(value) = definedExternally
    var frameRate: Int? get() = definedExternally; set(value) = definedExternally
    var repeat: Int? get() = definedExternally; set(value) = definedExternally
}

The object expression above gets turned into this:

  function tuxemon$lambda$lambda$lambda$ObjectLiteral_0(this$) {
    this.key_jq6mq5$_0 = 'misa-left-walk';
    this.frames_93w5a6$_0 = this$.anims.generateFrameNames('atlas', json([to('prefix', 'misa-left-walk'), to('start', 0), to('end', 3), to('zeroPad', 3)]));
    this.frameRate_2yfl4x$_0 = 10;
    this.repeat_j15sdp$_0 = -1;
  }
  Object.defineProperty(tuxemon$lambda$lambda$lambda$ObjectLiteral_0.prototype, 'key', {get: function () {
    return this.key_jq6mq5$_0;
  }, set: function (key) {
    this.key_jq6mq5$_0 = key;
  }});
  Object.defineProperty(tuxemon$lambda$lambda$lambda$ObjectLiteral_0.prototype, 'frames', {get: function () {
    return this.frames_93w5a6$_0;
  }, set: function (frames) {
    this.frames_93w5a6$_0 = frames;
  }});
// snip

Defining key, frames, frameRate, and repeat as prototypal properties is problematic for libraries that do hasOwnProperty checks, such as Phaser, in GetValue. Basically, the above code actually doesn’t work – because Phaser checks for attributes in the AnimationConfig using hasOwnProperty, it never sees any of the attributes I’m passing in in the above example.

The workaround is moderately annoying – create a dynamic object using js("{}") and “cast” it to an AnimationConfig.


#2

That is how kotlin supposed to work. Idea is to simplify object model by having only properties and no fields.
https://kotlinlang.org/docs/reference/properties.html#backing-fields
Since this is big mismatch with actual implementation and any external system/language there are many inconvenient edge cases. Same goes for nullability too btw.


#3

I agree with the OP that this is a bit annoying. Is there a convenient annotation that we can use to make it more compatible with JS libraries?


#4

On the one hand, I agree that this kinda how Kotlin is supposed to work. On the other hand, I wish the codegen was different for external interfaces or definedExternally. Don’t use the backing fields model, perhaps, but just use naive simple fields.

Since, as we’re supposing, there’s no actual bug here, yet the designed behavior does break external libraries, I’m trying to find what the optimal alternative is, and then perhaps we can document that somewhere.

I see many options.


Option 1: Switch to external class

At least in my case, there IS no actual concrete class – it’s just an anonymous {} that the interface is describing. So instantiation will fail at runtime. Doesn’t work.

Option 2: Switch to a non-external class

This will generate the same breaking backing fields.

Option 3: Use @nativeGetter and @nativeSetter

No part of

var key: String? get() = definedExternally; set(value) = definedExternally

is actually a valid target for those annotations, plus they’re deprecated. Doesn’t work.

Option 4: Use extension methods to create vars, instead of properties

Just add

inline var AnimationConfig.key: String? get() = definedExternally; set(value) = definedExternally

right? Actually this isn’t valid either because definedExternally can’t be used from something that’s not external, and while AnimationConfig is external, the compiler doesn’t recognize that. Doesn’t work.


Option 5: Create a builder object and cast it to the interface

I have a couple helper methods now that look like this:

inline fun <T> jsApply(init: dynamic, cb: T.() -> Unit): T {
    cb(init.unsafeCast<T>())
    return init.unsafeCast<T>()
}

inline fun <T> jsLet(init: dynamic, cb: (T) -> Unit): T {
    cb(init.unsafeCast<T>())
    return init.unsafeCast<T>()
}

which lets you write code at the callsite that looks like this:

anims.create(jsApply {
    frameRate = 10
    repeat = -1
})

which isn’t too bad, but also confusing to new users (since this is part of a library I’m writing). If they don’t use jsApply, the code breaks. Works, but not ideal.

Option 6: Wrap Option 5 in a nicer pattern

If I create a little helper “constructor” like this:

fun AnimationConfig(cb: (AnimationConfig.() -> Unit) = {}): AnimationConfig = jsApply(cb)

Then it can be called either like this:

anims.create(AnimationConfig().apply {
    frameRate = 10
    repeat = -1
})

which lets them use apply or also, OR it can be called like this:

anims.create(AnimationConfig {
    frameRate = 10
    repeat = -1
})

which almost even looks nice.

This still has the problem that if users of this API don’t know to call this, and try implementing the AnimationConfig interface instead, they’re in trouble. But at least this is easy to remember and looks vaguely idiomatic. It also means I need to create this helper method for most of the external interfaces I’m modeling, which is somewhat inconvenient.