Hello everyone. I am currently experiencing problems using `List<T>` on my `@JsExport` models...
a
Hello everyone. I am currently experiencing problems using
List<T>
on my
@JsExport
models. Not only the fields mangled but it almost unusable in
kotlin/js
. At the moment I am exporting every
List<T>
propery with a field that exposes an array, like so
Copy code
class Person(val names: List<String> = listOf("John","Doe")) {
  val namesArray get() = names.toTypedArray()
}
but that is now becoming tedious. So, any one experiencing this? I wouldn't want my properties to not be Lists, but would need them to easily be consumable from the JS side when we export our library I have a feeling if the kotlin-stdlib-js could export type bindings for all collections, List, Map, Set e.t.c, It could have been of much help. But I wanna know how others are tackling this. Thoughs?
h
Yeah, we have the same problem. We switched to
Array
in the shared module, but this is also not perfect, because now equals does not use the content. So in every test we use
toList
again.
g
We're creating a Facade for JS, only exporting Arrays instead of collections, and for constructors with default values, we create different constructors and don't use default values at all. Kind of like this:
Copy code
class Person(val names: List<String> = listOf("John","Doe")) // commonMain

// jsMain
@JsExport
class PersonJs(val names: Array<String>) {
    @JsName("default") constructor() : this(listOf("John", "Doe"))
    val namesArray get() = names.toTypedArray()
}
We're also working with KSP to auto-generate these facades as it's more complex if you want proper import/export behaviours.
👀 1
a
Would you share some KSP snippets for this? @Grégory Lureau
Or if its open source, can I have a link? If its private I would also understand
g
For example, when generating with KSP we can have a more complex facade that is just delegating
Copy code
// jsMain
@JsExport
class PersonJs internal constructor(val common: Person) {
    @JsName("create") constructor(val names: Array<String>) : this(Person(listOf("John", "Doe")))
    val namesArray get() = common.namesArray.toTypedArray()
}
To be honest we're not stable on what we really want on the facade. There are a lot of limitations we're learning (like for generics for example) and we need to have a proper understanding. I'd love to open-source that when it's stable (don't expect a perfect solution but it may be a starting point).
Still need to negotiate the open-sourceing with my company.
a
I see. Thanks also for sharing your approach. I know have an alternate perspective
a
A separate JS Facade is how we're doing it too - but manually written 😔 In @Greg Steckman ‘s Person example there’s also more than one option • wrap the class (what he did - requires a manual constructor definition and doesn't allow JS to pass in objects as JSON) • Create an external interface and map to/from the data class (gets you Typescript definitions for a standard JS Object so JS can pass in a normal JSON) • Use kotlinx.serialization and encodeToDynamic/decodeFromDynamic (JS can pass in JSON, converts Lists to Arrays, less per-class manual work, but higher bundle size and no TS definitions)
👍 1
g
I'd love to read a more in-depth article on these different approaches. Actually it feels quite complicated to have something with a good interop + low bundle size. Looks like wrapping is also making a notably higher bundle size, but other options feels a bit off to me, meaning the logic in multiplatform project will need to take into account js restrictions. For example, I presume the 3rd option with serialization means you copy the data: if you have a method on a data class, or expose a property with a getter that has a dynamic value, then the exposed object is "only" a snapshot of the dynamic value and will not expose the method?
a
I'm actually working on a (light) writeup to compare those options right now! But yeah you're absolutely right: the serialization approach works when you're exposing barebones immutable value-based data classes, but not when you have more complex things
🎉 1
g
@ankushg Did you mean to "@" me above? Not sure I contributed a Person example 🙂
a
Ahh sorry, I didn't mean to @ you, must have grabbed the wrong person on mobile!
a
Okay, I have come to a solution that I feel I am comfortable with and can surely be easily incorporated in the stdlib when it comes to interoperability with collections for kotlin/js. Code in thread
I have new interfaces like so Collection.kt // commonMain
Copy code
import kotlin.collections.Collection as KCollection

expect interface Collection<out E> : Iterable<E>, KCollection<E>
Collection.kt //JsMain
Copy code
@file:JsExport
@file:Suppress("NON_EXPORTABLE_TYPE", "WRONG_EXPORTED_DECLARATION")

package kotlinx.collections.interoperable

import kotlin.collections.Collection as KCollection

actual interface Collection<out E> : Iterable<E>, KCollection<E> {
    fun toArray(): Array<out E> {
        val array: Array<in Any?> = Array(size) { null }
        forEachWithIndex { e, index -> array[index] = e }
        return array as Array<out E>
    }
}
List.kt // CommonMain
Copy code
@file:JsExport
@file:Suppress("NON_EXPORTABLE_TYPE", "WRONG_EXPORTED_DECLARATION")

package kotlinx.collections.interoperable

import kotlinx.collections.interoperable.serializers.ListSerializer
import kotlinx.serialization.Serializable
import kotlin.js.JsExport
import kotlin.collections.List as KList

@Serializable(with = ListSerializer::class)
interface List<out E> : Collection<E>, KList<E>
I also have
MutableList<E>:List<E>
, have,
Set
and
MutableSet
as well. which do resemble the above code snippet. I have tested this and it does work, and all my consumers just have to call
toArray()
to fully easily use it with javascript/typescript.
I also have ListBuilders for which return instances of the same`kotlinx.collections.interoperable` instead those of
kotlin.collections
. I strongly believe this can be done in the stdlib and will surely ease the interop saga in the js community
👍 2
If any one would be interested, I intend to publish this as a maven artifact to maven central.
👍 1
g
Nice! For the KSP generator I mentioned earlier I got clearance to open-source it. It's pretty rough today, see that as an experimentation, but if it makes sense to you, I'll be happy to contribute more in that way. https://github.com/glureau/KustomExport (disclaimer: that's my first open-sourcing of a draft on a KSP techno I'm learning now, on a JS IR compiler not stable yet...)
👀 1
a
That looks super cool @Grégory Lureau ! I was (and still am) researching piggybacking off of `kotlinx.serialization`'s `SerialDescriptor`s for this (because we make API calls from within KMP and need to deserialize them anyway), but this seems super interesting!
g
Thanks! I think it'll come with several limitations, mainly it will probably not be able to wrap a class with generics (
MyClass<MyData>
is fine, but
MyClass<T: MyData>
is not) and it has a serious increase in JS bundle size due to multiple classes required for wrapping. I think an even better approach could be to rework directly js and d.ts files directly : detect the unwanted types exposed in typescript and rewrote both files with a better facade without requiring a big increase in bundle size. No idea how to do that rn.
g
This is great, @Grégory Lureau! Despite limitations, it tackles the basics (for us, that means
List
,
interface
, and
Long
). Are you planning to publish this? I've been trying to use this as source dependency or project dependency (via submodule).
g
It's work in progress, I'd not recommend using it today, but you can follow the project here https://github.com/deezer/KustomExport . I should be able to publish a snapshot in the next days/weeks. Don't hesitate to create issues with your requirements/questions 😉 (NB: due to some KSP issues, 1st release could be postponed to wait for 1.0.2)
🎉 1