Search code examples
genericskotlintyping

How to cast to a type that is only known at runtime


Is there a (elegant) way to cast to a type that is only available at runtime? Pls check this out:

data class Foo<T> (
    var value: T
)

val foos = listOf<Foo<*>>(
    Foo(value = "I a a String"),
    Foo(value = 1),
    Foo(value = true)
)

val anyTypes = listOf<Any>("String", 1, false)

fun main() {

    foos.forEachIndexed() { idx, foo ->
       foo.value = anyTypes[idx]
    }

    foos.forEach(::println)
}

Oh and it is not possible to create a list for each type separately! ;-)


Solution

  • Thanks to erasure, the type argument of each Foo isn't known at runtime at all.

    There is no perfect way to solve this problem, because using generics like this is inherently unsafe. There may be a way to rethink your solution to avoid this altogether (mutating values of star-projected types is generally a bad idea, which is why the type system prevents you from assigning anything but null to values of upper-bounded invariant types.)

    You'll have to provide the type itself as an argument to each Foo:

    data class Foo<T : Any> (
        val type: KClass<T>,
        var value: T
    )
    
    val foos = listOf<Foo<*>>(
        Foo(String::class, value = "I a a String"),
        Foo(Int::class, value = 1),
        Foo(Boolean::class, value = true)
    )
    
    val anyTypes = listOf<Any>("String", 1, false)
    
    ...
    foos.forEachIndexed { idx, foo ->
        @Suppress("UNCHECKED_CAST")
        (foo as Foo<Any>).value = foo.type.cast(anyTypes[idx])
    }
    
    foos.forEach(::println)
    

    You could also omit the KClass argument and go without the cast, but then you risk silently accepting values of the wrong type, which will only fail when the value is accessed and used.