TypeMapping JavaScript To Kotlin and vice versa


#1

Hallo Everyone,

I’m learning Kotlin and for that reason I’m developing a browser extensions for firefox.
So therefore I’m making myself familiar with KotlinJs.

Now I’ve got a question about interop type mapping. This seems to confuse me a lot. To give you an example. Here is a class I’m working on currently, in order to store data for a browser extension


package core.storage

import core.utils.jsObject
import webextensions.browser
import kotlin.js.Promise

object StorageService {

    const val INFO_KEY = "StorageInfos";

    private inline fun List<StorageInfo>.toMap() = this.associateBy({ it.key }, { it }).toMutableMap()
    private inline fun Map<String, StorageInfo>.toList() = this.map { it.value }

    fun save(entry: StorageEntry): Promise<*> {

        val pair = jsObject()

        return loadInfos().then { infoList ->
            val infoMap = infoList.toMap()
            infoMap[entry.info.key] = entry.info
            pair[INFO_KEY] = infoMap.toList()
            pair[entry.info.key] = entry
        }.then {
            browser.storage.asDynamic().sync.set(pair)
        }
    }

    // does not work
    fun loadInfosBroken(): Promise<List<StorageInfo>> {
        val resultsPromise = browser.storage.asDynamic().sync.get(INFO_KEY) as Promise<*>
        return resultsPromise.then {
            it.asDynamic()[INFO_KEY] as List<StorageInfo>? ?: ArrayList()
        }
    }

    // works
    fun loadInfos(): Promise<List<StorageInfo>> {
        val resultsPromise = browser.storage.asDynamic().sync.get(INFO_KEY) as Promise<*>
        return resultsPromise
            .then { it.asDynamic()[INFO_KEY] as Array<StorageInfo>? ?: emptyArray() }.then { it.toList() }
    }

    fun clear(): Promise<*> {
        return browser.storage.asDynamic().sync.clear() as Promise<*>
    }

}

Method “loadInfosBroken” does not work. Somehow I can store data from a list but not retrieve it the same way. However I can retrieve list data as an array.

I wanted to work with a map, but also that seams to be confusing and not straight forward.

So my questions regarding that issue are:

  • Is there a good comprehensive guide which explains how objects from js to kotlin and vice versa do map?
  • Are there API’s which make working with these interop scenarios easiery?

#2

Another question.

Here in this line:

val resultsPromise = browser.storage.asDynamic().sync.get(INFO_KEY) as Promise<*>

I’m casting the promise to (Promise<out Any?>)

I tried to cast it to Promise<dynamic> in order to avoid the asDynamic Call later. But that is not working as intelijii tells me that the inference is not working in that case.

So if you have an idea how to make the code less verbose, you are welcome :wink:


#3

Now I’ve run into the next issue with types and kotlinJs … somehow the kotlin type and javascript type are not always mapped the same way, even though they appear like they are the same on the kotlin side

Here is my StorageInfo clas:

import kotlin.js.Date

data class StorageInfo(val key : String, val size : Int, val updated : Date)

So I’m using the date-class for java script of the kotlinjs standard library.
For printing the datetime I wrote myself a function:

fun Date.ToFormatedDateString() =
        if (jsTypeOf(this) == "object")
            "${getFullYear()}-${getMonth()}-${getDay()} ${getHours()}:${getMinutes()}:${getSeconds()}"
        else jsTypeOf(this)

Here is an example how it looks with data loaded from the storage

The last line is not out of the storage but a new object.So if I call a method of the date class it crashes,
even though it appears to be type-safe.

Does anyone have an Idea how I can fix the serialization/deserialization?

Is it maybe more advisable to work with a json serialization framework? Are there kotlinjs annotations I could use to influence serialization/deserialization? I have not much experience with java script but I reckon JsObject and Json are not entirely the same, or are they?


#4

Try kotlinx.serialization library from JetBrains. Make your data class serializable by adding @Serializable annotation. You can also easily create custom serializer for kotlin.js.Date.


#5

@rjaros

Thank your for the suggestions. I use the serializer and now it works.

Here the final code

DateSerializer

package core.storage

import kotlinx.serialization.*
import kotlinx.serialization.internal.StringDescriptor
import kotlin.js.Date

@Serializer(forClass = Date::class)
object DateSerializer : KSerializer<Date> {
    override fun deserialize(input: Decoder): Date {
        return Date(input.decodeDouble())
    }

    override val descriptor: SerialDescriptor =
        StringDescriptor.withName("Date")

    override fun serialize(output: Encoder, obj: Date) {
        output.encodeDouble(obj.getTime())
    }
}

StorageInfo:

@Serializable
data class StorageInfo(val key : String, val size : Int,
                       @Serializable(with=DateSerializer::class)
                       val updated : Date)

StorageService

package core.storage

import kotlinx.serialization.*
import kotlinx.serialization.json.JSON
import core.utils.jsObject
import webextensions.browser
import kotlin.js.Promise

object StorageService {

    const val INFO_KEY = "StorageInfos";

    private inline fun List<StorageInfo>.toMap() = this.associateBy(StorageInfo::key).toMutableMap()

    fun save(entry: StorageEntry): Promise<*> {

        val pair = jsObject()

        return loadInfos().then {
            val infoMap = it.toMap()
            infoMap[entry.info.key] = entry.info
            pair[INFO_KEY] = JSON.stringify(StorageInfo.serializer().list,infoMap.values.toList())
            pair[entry.info.key] = entry
        }.then {
            browser.storage.sync.set(pair)
        }
    }
    
    fun loadInfos(): Promise<List<StorageInfo>> {
        val resultsPromise = browser.storage.sync.get(INFO_KEY)
        return resultsPromise.then {
            try {
                JSON.parse(StorageInfo.serializer().list, it[INFO_KEY]) }
            catch(e:dynamic) {
                ArrayList<StorageInfo>()
            }
        }
    }

    fun clear(): Promise<*> {
        return browser.storage.sync.clear()
    }

}