Hot new strat for defining type-union properties in Kotlin/JS
// SomeConfig is an external TypeScript interface
// it has a property with a type-union type, but don't implement it as a member...
external interface SomeConfig {
// multiProp: number | string | Extension | ((string) => Extension | null)
}
external interface Extension // some external TypeScript interface
// instead, implement multiProp as an extension property using a custom delegate, TypeUnion
// and specify the types within the union
val SomeConfig.multiProp by TypeUnion<Int, String, Extension, (String) -> Extension?>()
// example usage
fun foo(config: SomeConfig) {
// thanks to some assignment plugin magic, the types specified in the TypeUnion delegate can be assigned
config.multiProp = 123
config.multiProp = "a b c"
config.multiProp = object : Extension {}
config.multiProp = { arg -> if (arg == "blah") object : Extension {} else null }
config.multiProp = true // ERROR: Boolean is not specified TypeUnion
}
-
Apply and activate Kotlin Assigment Plugin
(
org.jetbrains.kotlin-wrappers:kotlin-js
provides some helpful utils, likejso()
)// build.gradle.kts plugins { kotlin("multiplatform") version "1.9.0" id("org.jetbrains.kotlin.plugin.assignment") version "1.9.0" } kotlin { js(IR) sourceSets { val jsTest by getting { dependencies { val kotlinWrappersVersion = "1.0.0-pre.601" implementation(project.dependencies.platform("org.jetbrains.kotlin-wrappers:kotlin-wrappers-bom:$kotlinWrappersVersion")) implementation("org.jetbrains.kotlin-wrappers:kotlin-js") } } } assignment { annotation("AssignmentOverload") }
-
Define TypeUnion helpers
//TypeUnions.kt import TypeUnionProperty.* import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty fun interface TypeUnion<out TUP : TypeUnionProperty> : ReadOnlyProperty<Any?, TUP> fun <A> TypeUnion( @Suppress("UNUSED_PARAMETER", "LocalVariableName") _marker: FnMarker1 = FnMarker1 ) = TypeUnion { thisRef: Any?, prop -> TypeUnionProperty1<A>(thisRef, prop.name) } fun <A, B> TypeUnion( @Suppress("UNUSED_PARAMETER", "LocalVariableName") _marker: FnMarker2 = FnMarker2 ) = TypeUnion { thisRef: Any?, prop -> TypeUnionProperty2<A, B>(thisRef, prop.name) } fun <A, B, C> TypeUnion( @Suppress("UNUSED_PARAMETER", "LocalVariableName") _marker: FnMarker3 = FnMarker3 ) = TypeUnion { thisRef: Any?, prop -> TypeUnionProperty3<A, B, C>(thisRef, prop.name) } fun <A, B, C, D> TypeUnion( @Suppress("UNUSED_PARAMETER", "LocalVariableName") _marker: FnMarker4 = FnMarker4 ) = TypeUnion { thisRef: Any?, prop -> TypeUnionProperty4<A, B, C, D>(thisRef, prop.name) } fun interface TypeUnionPropertyDelegateProvider<TUP : TypeUnionProperty> : PropertyDelegateProvider<Any?, TUP> { override fun provideDelegate(thisRef: Any?, property: KProperty<*>): TUP = provideDelegate(thisRef, property.name) fun provideDelegate(thisRef: Any?, propertyName: String): TUP } annotation class AssignmentOverload @AssignmentOverload sealed class TypeUnionProperty { protected abstract val owner: dynamic protected abstract val propertyName: String // create some dummy discriminators to use as args in the TypeUnion functions, // because Kotlin can't differentiate between generic functions unless the args are different object FnMarker1 object FnMarker2 object FnMarker3 object FnMarker4 companion object } abstract class TypeUnionProperty1<A> : TypeUnionProperty() { fun assign(value: A) { owner[propertyName] = value } } abstract class TypeUnionProperty2<A, B> : TypeUnionProperty1<A>() { fun assign(value: B) { owner[propertyName] = value } } abstract class TypeUnionProperty3<A, B, C> : TypeUnionProperty2<A, B>() { fun assign(value: C) { owner[propertyName] = value } } abstract class TypeUnionProperty4<A, B, C, D> : TypeUnionProperty3<A, B, C>() { fun assign(value: D) { owner[propertyName] = value } } fun <A> TypeUnionProperty1(owner: dynamic, propertyName: String) = object : TypeUnionProperty1<A>() { override val owner: dynamic = owner override val propertyName: String = propertyName } fun <A, B> TypeUnionProperty2(owner: dynamic, propertyName: String) = object : TypeUnionProperty2<A, B>() { override val owner: dynamic = owner override val propertyName: String = propertyName } fun <A, B, C> TypeUnionProperty3(owner: dynamic, propertyName: String) = object : TypeUnionProperty3<A, B, C>() { override val owner: dynamic = owner override val propertyName: String = propertyName } fun <A, B, C, D> TypeUnionProperty4(owner: dynamic, propertyName: String) = object : TypeUnionProperty4<A, B, C, D>() { override val owner: dynamic = owner override val propertyName: String = propertyName }
// TypeUnionTests.kt
import js.core.jso
import kotlin.test.Test
import kotlin.test.assertEquals
/**
* ```typescript
* interface SomeConfig {
* multiProp: number | string | Extension | ((string) => Extension | null)
* }
* ```
*/
external interface SomeConfig
val SomeConfig.multiProp by TypeUnion<Int, String, Extension, (String) -> Extension?>()
/**
* ```typescript
* type Extension = // ...
* ```
*/
external interface Extension
class TypeUnionTest {
@Test
fun testTypeUnions() {
assertEquals("{\"multiProp\":123}", JSON.stringify(
jso<SomeConfig>{
multiProp = 123
}
))
assertEquals("{\"multiProp\":\"a b c\"}", JSON.stringify(
jso<SomeConfig>{
multiProp = "a b c"
}
))
assertEquals("{\"multiProp\":{\"x_1\":\"123\"}}", JSON.stringify(
jso<SomeConfig>{
multiProp = object : Extension {
val x = "123"
}
}
))
}
}