kotlingenerics

Kotlin type inference issue with createPropertyReader and mismatched types


I am working on a Kotlin project where I have a function createPropertyReader that uses a single type parameter T to enforce type safety between a property and a read function. Here is the relevant code:

inline fun <R, reified T> createPropertyReader(
    property: KProperty1<R, T>,
    noinline readFunction: (R) -> T
): PropertyReader<R, T> {
    return PropertyReader(property, readFunction)
}

class PropertyReader<R, T>(
    val property: KProperty1<R, T>,
    val readFunction: (R) -> T
)

The function is supposed to enforce that the type of property matches the type returned by readFunction. However, when I use it like this:

val x = createPropertyReader(User::id) { readString() }

It does not give a compile-time error, even though User::id is of type Uuid, and readString() returns String. Instead, Intellij shows the inferred type of x as PropertyReader<User, {Comparable<{String & UUID}> & Serializable}!>.

I tried to write a generic function which takes a class property and a lambda function which returns a value that is assignable to the same property. However, I expected a compile-time safety which was not the case.

I expected a compile-time error because the types do not match. Adding reified to the type parameter T did not make any difference. Why does Kotlin allow this mismatch without a compile-time error? Is there a way to enforce stricter type checking in this scenario?


Solution

  • This is similar to the situation here.

    {Comparable<{String & UUID}> & Serializable}! is indeed a valid type for T such that the call typechecks. It's just that you don't want it to find a type like this.

    As I said in the linked answer, you just need the T to appear in a parameter type of readFunction. This adds additional constraints to what T can be.

    So you can just add a T? parameter and pass null.

    fun <R, T> createPropertyReader(
        property: KProperty1<R, T>,
        readFunction: (R, T?) -> T
    ): PropertyReader<R, T> {
        return PropertyReader(property, { readFunction(it, null) })
    }
    

    Because readFunction now has 2 parameters, this means that you can't use it at the call site anymore. Consider using (Pair<R, T?>) -> T if that's undesirable.