Search code examples
kotlingenericsenums

kotlin get enum value by it's generic type in case it is enum


I'm working on a task to create an API to application string properties which can be represented as Enum, Int, String and probably some more types.

So I tried to write kind of this class:

object Configuration {
    inline fun <reified T> get(context: Context, propertyName: String): T {
        val propertyStringValue = context.config[propertyName]
            ?: throw InvalidConfigurationException("Unable to access property $propertyName")
        if (T::class.isSubclassOf(Enum::class)) {
            // how can I convert it to enum by it's class name?
        } else if (T::class.isSubclassOf(String::class)) {
            // convert to string
        } else if (T::class.isSubclassOf(Integer::class)) {
            // convert it integer
        }
    }
}
enum class BuildingLevel{
    FIRST,
    SECOND,
    ...
}

enum class StreetType {
    OPEN,
    ISOLATED,
    ...
}

Given that property name is STREET_TYPE and context.config["STREET_TYPE"] return OPEN I expect Configuration.get<StreetType>("STREET_TYPE") to return enum entry StreetType.OPEN and Configuration.get<String>("STREET_TYPE") to return OPEN string.

What should I do in enum if branch if I want to convert string to enum entry having reified generics representing certain enum class?


Solution

  • This is one of cases where enforced strong typing actually makes more harm than good. We know T is an enum, however, there is no way to "cast" a type parameter or add upper bounds to it. So even if we know that calling enumValueOf<T>(propertyStringValue) should be fine to do, compiler doesn't let us do it.

    To simplify we can say the task is to implement this function:

    inline fun <reified T> untypedEnumValueOf(name: String): T = ???
    

    I believe there is no clean way to implement it as of Kotlin 2.0.20. There are some hacks though.

    First of all, in Kotlin we can suppress many not only warnings, but even errors:

    inline fun <reified T> untypedEnumValueOf(name: String): T {
        @Suppress("UPPER_BOUND_VIOLATED")
        return enumValueOf<T>(name)
    }
    

    Unfortunately, by suppressing errors we go to the land of dragons. It may compile, it may not. As a matter of fact, this code worked correctly in Kotlin 1.x, but it no longer compiles in 2.x with some silly message that String can't be String. It is probable this is just a bug and will be fixed in future versions of Kotlin. We can even argue this solution is the cleanest as we simply do what we have to do and we tell the compiler to not disturb us with its silly restrictions. But well, it doesn't work.

    Similarly, we can workaround the above problem by using enumEntries/enumValues instead of enumValueOf. This way we avoid passing the String param which caused the problem in the first place:

    inline fun <reified T> untypedEnumValueOf(name: String): T {
        @Suppress("UPPER_BOUND_VIOLATED")
        return enumEntries<T>().single { (it as Enum<*>).name == name }
    }
    

    This code works in Kotlin 2.x, but it is a convoluted variant of the first. Also, there is a risk it will stop working in future versions of the Kotlin compiler and/or for specific compile targets, similarly to the first solution.

    Finally, we can use the reflection API. This is probably slower and not as elegant as simply getting the enum by name, but it should be a rather reliable approach:

    inline fun <reified T> untypedEnumValueOf(name: String): T {
        return T::class.staticFunctions
            .single { it.name == "valueOf" }
            .call(name) as T
    }