When expression on sealed interface requires unnecessary branches

Consider the following split of a union model into interface and implementation:

sealed interface Model
interface Case0 : Model
interface Case1 : Model

sealed class ModelImpl : Model
object Case0Impl : ModelImpl(), Case0
object Case1Impl : ModelImpl(), Case1

fun createModel(): Model = Case0Impl

And here comes the usage:

fun main() {
    val model: Model = createModel()
    val res = when(model) {
        //...
    }
    println(res)
}

The when expression must be exhaustive. When triggering completion in IJ, it will insert the following branches:

val res = when(model) {
    is Case0 -> TODO()
    is Case1 -> TODO()
    Case0Impl -> TODO()
    Case1Impl -> TODO()
}

But the last two branches matching on the implementation can never be reached. When removing them, IJ is complaining with a non-exhaustive error (but it should be because Model and ModelImpl is sealed). It gets even worse when making the implementation classes private in the file. Then it seems to be impossible to write a non-trivial exhaustive when expression without else branch.

Is this a bug or am I missing something?

2 Likes

Seems to be a shortcoming of the compiler.

Consider the following code, which compiles without error:

sealed interface Model
interface Case0 : Model
interface Case1 : Model

object Case0Impl :  Case0
object Case1Impl :  Case1

val res = when(model){
    is Case0 -> TODO()
    is Case1 -> TODO()
}

However, this code will not compile, stating when is not exhaustive:

sealed interface Model
interface Case0 : Model
interface Case1 : Model

object Case0Impl :  Model, Case0
object Case1Impl :  Model, Case1

val res = when(model){
    is Case0 -> TODO()
    is Case1 -> TODO()
}

As such, it seems the compiler will only accept exhaustive when branches that have cases for all classes that directly implement a sealed class or interface.

We can prove this by introducing an extra, unnecessary check for ModeImpl:

val res = when(model){
    is Case0 -> TODO()
    is Case1 -> TODO()
    is ModelImpl -> TODO()
}

Thanks for the simplified reproducer. I created an issue here: https://youtrack.jetbrains.com/issue/KT-55658

Here’s a runnable example if anyone is interested.

sealed interface Model
interface Case0 : Model
interface Case1 : Model

// Try uncommenting to see error
object Case0Impl : Case0 //, Model
object Case1Impl : Case1 //, Model

fun main() {
    val model: Model = Case0Impl
    val res = when(model){
        is Case0 -> "1"
        is Case1 -> "2"
    }
    println(res)
}