Is there an elegant way to check if an object's type belongs to the list of types?

Viewed 2280

When working with strings you can check if a string belongs to a string set like this:

if (a in listOf("ab", "abab")) {
    // do something
}

Or:

when (a) {
    in listOf("akshd", "sd") -> {
        // do something
    }
}

Now, let's imagine you have a sealed class:

sealed class SealedClass {
    class Subclass1 : SealedClass()
    class Subclass2 : SealedClass()
    class Subclass3 : SealedClass()
}

There's a nice way to check if some object b is in the subset of these types. If you use when expression you can write something like this:

when (b) {
    is SealedClass.Subclass1, is SealedClass.Subclass2 -> {
        // do something
    }
}

Things are getting complicated if you want to use if expression. You can write something like this:

if (b is SealedClass.Subclass1 || b is SealedClass.Subclass2) {
    // do something
}

Or this:

if (
    b::class in listOf(
        SealedClass.Subclass1::class,
        SealedClass.Subclass2::class
    )
) {
    // do something
}

Both solutions above are quite clumsy. Is there a more elegant way to express such a check when using the if statement?

3 Answers

I don't believe there is a way around that.

Creating a custom solution could be counterproductive as type-safety will be an issue. If a type that is incompatible is used the code will run but you may end with a run-time error. However, if you use is directly you will get a compile-time error saying "Incompatible types".

The clean way to go about this would be to create a method or property in the sealed class for the same. With proper naming, the code will be much easier to read. For example:

sealed class Animal {
    class Cat : Animal()
    class Dog : Animal()
    class Tiger : Animal()

    val isDomestic
        get() = this is Cat || this is Dog
}

Usage:

if (animal.isDomestic) {
    // do something
}

If the same list of accepted types is used frequently, you can define it somewhere and use it together with this simple checkType function:

import kotlin.reflect.KClass

private val acceptedTypes: List<KClass<out Comparable<*>>> =
    listOf(Boolean::class, Int::class, String::class)

fun main() {
    val value1 = "This is a string"
    val value2 = 123_456L

    if (checkType(value1, acceptedTypes)) {
        println("Value \"$value1\" has type \"${value1::class}\" and " +
                "is in the list of accepted types $acceptedTypes.")
    }

    if (!checkType(value2, acceptedTypes)) {
        println("Long value is not in the list of accepted types!")
    }
}

private fun checkType(value: Any, acceptedTypes: List<KClass<out Comparable<*>>>):
        Boolean = value::class in acceptedTypes

With an extension function for the Any type:

fun main() {
    val value1 = "This is a string"
    val value2 = 123_456L

    if (value1.checkType(acceptedTypes)) {
        println("Value \"$value1\" has type \"${value1::class}\" and " +
                "is in the list of accepted types $acceptedTypes.")
    }

    if (!value2.checkType(acceptedTypes)) {
        println("Long value is not in the list of accepted types!")
    }
}

fun Any.checkType(acceptedTypes: List<KClass<out Comparable<*>>>) = this::class in acceptedTypes

Are you just trying to check if the type is either Subclass1 or Subclass2, but not Subclass3? You can nest sealed classes if you want to enforce more complex groups of types, like:

sealed class SealedClass {
   sealed class CoolSubtype : SealedClass() {
       class Subclass1 : CoolSubtype()
       class Subclass2 : CoolSubtype()
   }
   class Subclass3 : SealedClass()
}

that way you can do subclass is CoolSubtype. You don't need to nest the class definitions if you don't want. You can also create empty interfaces and decorate the subclasses with those, if you want to give them multiple types.


If you're creating arbitrary groups then yeah, I think your only option really is a collection of ::class objects with an in check. You can neaten that up with a function of course - make it infix if you like!

infix fun <T : Any> Collection<KClass<out T>>.includes(item: Any) = item::class in this

// same function, just reversed so you can express it the other way around
infix fun <T : Any> Any.memberOf(types: Collection<KClass<out T>>) = this::class in types

fun main() {
    val types = listOf(Subclass1::class, Subclass2::class)
    val one = Subclass1()
    val three = Subclass3()
    println("One: ${types includes one}")
    println("Three: ${three memberOf types}")
}
> One: true
> Three: false

Or you could overload the in operator if you wanted:

operator fun <T : Any> Collection<KClass<out T>>.contains(item: KClass<T>) = item in this
println("One: ${one::class in types}")
> True

but you'd need to pass in a KClass for that, if you overload it for an Any parameter you're basically changing the behaviour of in everywhere (and you can't use it in the function body either, because it would be recursively calling itself)

Related