How to avoid this surprising Map behavior with external JS classes


I’m trying to wrap an external Svelte class, Store, defined in store.js.

My initial class looks like this:

external class Store(initialData: Map<String, Any?> = definedExternally) {
    fun get(): Map<String, Any?>
    fun set(obj: Map<String, Any?>)
    // snip

The constructor accepts a map, assigns it to an internal field, which can then be updated using set and retrieved using get. (get returns the entire thing, while set merges in the new map into the current state).

Suppose I construct the Store like this:

Store(mapOf("currentPage" to "vis"))

And then I pass it in to my other Svelte component like this:

App(svelteOptions(store = store))

// elsewhere
inline fun svelteOptions(store: Store? = null): SvelteOptions {
    val o = js("({})")
    store?.let { o["store"] = it }
    return o.unsafeCast<SvelteOptions>()

Finally, off in my SideNavLink.html Svelte component (which is not Kotlin), I do this:{ currentPage: name });

Something surprising happens when, back in my Kotlin code, I have this:

store.onState { (changed) ->
    if (changed.currentPage) {
        console.log("Current page changed! is now ${store.get()["currentPage"]}")

The surprising thing: store.get()["currentPage"] always has the initial value that it was given in the constructor!!

This took me a little while to figure out, but the reason is this: I’m constructing the Store with an actual Kotlin Map object. When I call .set() from my non-Kotlin Svelte component, it’s attaching a property directly to this Map object. But then when I call .get()["whatever"], it’s not looking up the whatever property – it’s executing .get().get_11rb$(key) – looking up the key in the Kotlin Map, not whatever was set on the object itself.

There is a solution to this: change fun get(): Map<String, Any?> to fun get(): dynamic. But it IS a Map-shaped Object, just not a Kotlin/JVM map. Is there a solution here that doesn’t involve plastering dynamic all over my Store external class?