Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin - How to avoid casting of second variable in when() statement of a sealed class

Tags:

kotlin

I have a sealed class representing field types:

sealed class FieldDef {
    object StringField: FieldDef()
    class ListField(val element: FieldDef): FieldDef()
    class MapField(val children: Map<String, FieldDef>): FieldDef()

    // ... more field types
}

And I have a function that processes a pair of fields definitions.

    fun processSameTypes(fd1: FieldDef, fd2: FieldDef) {
        if (fd1::class == fd2::class) {
            when (fd1) {
                is MapField -> processMaps(fd1, (fd2 as MapField))
                is ListField -> processLists(fd1, (fd2 as ListField))
            }
        }
    }

The code as written is correct - given the logic the casts of the fd2 variable will always succeed.

But I feel there should be a way to express this without the cast, however if I try to remove the cast I get the error:

Kotlin: Type mismatch: inferred type is FieldDef but FieldDef.ListField was expected

Is there a better way to handle this such that I can remove the cast (the cast is not necessary).

If there is no better way of solving this, could you explain why the compiler is unable to infer the correct type of the fd2 variable. My logic here is that since the when is protected by the outer if and that FieldDef is a sealed class the compiler should be able to infer the type of fd2.

Kotlin Version: 1.5.30 (JVM)

Edit: Conclusion

I think it's fair to say that passing a class object around is idiomatic Java (and through its heritage Kotlin too), specifically that it is the way of representing types at Runtime. Hence it is something I would like to see the Kotlin compiler support in the future.

That said, it doesn't support it today, so I agree with @TreffnonX - that just sucking it up and throwing in the cast is the best solution - hence I am not going to change my code.

However I did ask for a way to solve the problem without using a cast and @Sweeper provided one - so I will accept that answer.

like image 778
DavidT Avatar asked Feb 03 '26 21:02

DavidT


1 Answers

Don't give an argument to when, so that you can check the types of multiple things.

// Now you can remove "if (fd1::class == fd2::class)" too
when {
    fd1 is FieldDef.MapField && fd2 is FieldDef.MapField -> 
        processMaps(fd1, fd2)
    fd1 is FieldDef.ListField && fd2 is FieldDef.ListField -> 
        processLists(fd1, fd2)
}

Alternatively, create your own inline function for checking both:

inline fun <reified TTarget> checkBothTypes(a: Any, b: Any, block: (TTarget, TTarget) -> Unit) {
    if (a is TTarget && b is TTarget) {
        block(a, b)
    }
}

Another way of writing this, to allow returning a result from the block:

inline fun <reified TTarget, TResult> checkBothTypes(
    type: KClass<TTarget>, a: Any, b: Any, block: (TTarget, TTarget) -> TResult
): TResult? {
    if (a is TTarget && b is TTarget) {
        return block(a, b)
    }
    return null
}

Usage:

checkBothTypes<FieldDef.ListField>(fd1, fd2) { l1, l2 ->
    processLists(l1, l2) // or use a function reference
    // ...
}
checkBothTypes<FieldDef.MapField>(fd1, fd2) { m1, m2 ->
    processMaps(m1, m2) // or use a function reference
    // ...
}

If you want to get a result from all the checks, use ?: to chain the calls together, creating a single expression.

Note that unlike when using when(...), these approaches cannot detect a missing check for a subclass of FieldDef.

like image 148
Sweeper Avatar answered Feb 05 '26 14:02

Sweeper