Should add better support for KType


#1

I was trying to do some testing on an API and was wanting to just check if a property was declared with the type it should be. The Kotlin reflection has the KType for precisely representing the type, but what it does not have is a way to get a KType to compare it to. In particular what I wanted was a way to get a KType from a type using something like ::type the way we have ::class as in:

inline fun <reified T> KCallable<*>.isOfType(): Boolean
    = T::type == returnType

so I could say

val isOfRightType = MyClass::myProperty.isOfType<List<Foo?>>()

Or perhaps you could just implement such a method on KType


#2

Sadly, this cannot be implemented as a method on KType because of type erasure on JVM. We may be able to introduce a language feature in the future to support this, however.

Currently, as a workaround, you can declare a function/property with that type somewhere in your code and compare the two types with ==:

fun f(): List<Foo?> = TODO()
...
val isOfRightType = MyClass::myProperty.returnType == ::f.returnType

#3

When I said as a method on KType I was thinking of something like a myType.isOfType<List<Foo>>(), but this would be simpler:

inline <reified: T> fun KType() : KType

Which of course cannot be easily implemented using normal Kotlin and might require some special trickery like the way javaClass() worked.

Then I could say something like this:

val isOfRightType = MyClass::myProperty.returnType == KType<List<Foo?>>()

But it would be handier to have something like <List<Foo?>>::type similar to ::class


#4

Here is something (probably incomplete) that I build based on the Stack Overflow answer in the comment:

// http://stackoverflow.com/questions/36253310/how-to-get-actual-type-arguments-of-a-reified-generic-parameter-in-kotlin
abstract class ObjectType<TType> {
    val kotlinType = toKotlinType((javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0])

    override fun toString() = kotlinType.toString()

    companion object {
        inline fun <reified TType : Any> t() = object : ObjectType<TType>() {}

        private fun toKotlinType(javaType: Type, typeArguments: List<KType> = emptyList()): KType {
            return when (javaType) {
                is Class<*> ->
                    javaType.kotlin.createType(typeArguments.map { KTypeProjection(KVariance.INVARIANT, it) })
                is ParameterizedType ->
                    toKotlinType(javaType.rawType, javaType.actualTypeArguments.map { toKotlinType(it) })
                is WildcardType ->
                    toKotlinType(javaType.upperBounds[0], typeArguments)
                else ->
                    throw IllegalArgumentException("Cannot convert Java type: $javaType")
            }
        }
    }
}

It returns an ObjectType<TObject> instead of KType so I can use it in functions similar to this without having to specify the type twice:

fun <T> doSomething(type: ObjectType<T>): T

The function above can be invoked like this:

val result = doSomething(t<List<String>>())

#5

I think having a full non-erased type system would be great. Especially if you added support for getting it from non-kotlin fields and classes.

fun Field.getFullType(declaredClassType: FullType = fullGenericType(this.getDeclaringClass()))
fun FullType.getGenericParent(parentClassOrInterface: (K)Class<*>)

And as an example:

class MiddleMap<K, V>(val middle: K) : Map<V, K>()
class SomeMap<T>(val some: T) : MiddleMap<Int, T>(10) {

fullTypeOf<SomeMap<String>>().getGenericParent(Map::class) // -> FullType<Map<String, Int>>
// -> Traverses up from SomeMap<String> to MiddleMap<Int, String> to Map<String, Int>

val t = fullTypeOf<SomeMap<List<String>>>()
val someField = SomeMap::class.java.getDeclaredField("some")
val middleField = MiddleMap::class.java.getDeclaredField("middle")

someField.getFullType(t) // -> FullType<List<String>>
// -> Maps type of field "some" to type of parameter "T" in SomeMap
middleField.getFullType(t) // -> Int
// -> Maps type of field "middle" to type parameter K, which then is mapped to the type passed in by SomeMap, which is Int

One big thing is that this would work with Java classes as well, mostly because I develop a small library that is primarily used Java code, and it does a lot of black magic with regards to types (it has an annotation-based serialization system)

(Just in case it helps, GSON has a lot of this, they could probably help you not tie your brain in a knot trying to piece apart Java’s type system like I did.)


#6

I just built what was needed for my use case. I don’t have the time to make a library which covers all possible cases (of which I am sure there are a lot).

My code also complete ignores the variance of the type parameters as it is not needed for what I am doing: checking if a type is a subtype of another type.

And I am not interested in Java interoperability.

I would love to see this as then I can drop my own (limited) code.


#7

I was thinking more as a possible language feature, not as a library feature.


#8

After a lot of experiments, and using another solution with more verbose syntax, I now came up with this. Note: This only works on the JVM, and uses the experimental function reflect():

import kotlin.reflect.jvm.reflect

@Suppress("CAST_NEVER_SUCCEEDS", "UNCHECKED_CAST")
fun <T> t() = null as T

fun <T> (() -> T).asKType() =
    reflect()?.returnType ?: throw IllegalStateException()

How do you use it? If a function needs a type, define a parameter accepting a parameterless function that returns the desired type as a result:

fun <TObject> needsType(functionWithTypeDefiningResult: () -> TObject) { ... }

Now, you can pass the type to the function using a short notation. Function t is generic, but because it is called in a lambda, the lambda will have a non-generic return type:

needsType({ t<String>() })
needsType({ t<List<String>>() })

In needsType(...) you can get the actual type using extension function asKType():

val type = functionWithTypeDefiningResult.asKType()

Never invoke the type-defining function that gets passed to you, because this will result in errors in most cases.