Smart cast of `as` expression

My case is like below:

fun foo(value: SuperType) {
    when (value as SealedType) {
        is SubType1 -> ... // value is SealedType
        is SubType2 -> ... // value is SealedType
    }
}

interface SuperType

sealed class SealedType : SuperType

class SubType1 : SealedType()
class SubType2 : SealedType()

I have an interface and a sealed class implementing this interface. At some point in code I have data typed as my interface, but I know it is my sealed type. I want to cast it and perform exhaustive when for specific subtypes.

Unfortunately, in this case the compiler does not smart cast value to a subtype. I understand this is because I switch on value as SealedType expression and not on the value itself. But because as is a part of the language, it always returns its left operand and it is related to the type system, I think it could make sense to implement smart-casting in such cases.

And another false negative (?) in smart casting, related to as expressions:

checkNotNull(animal as? Cat) { "" } // no smart cast
animal as? Cat ?: error("") // smart cast

This is interesting, because both cases are functionally very similar. In both the compiler could/should deduce that animal as? Cat can’t be null in order to get to a next line of code. But for some reason it does not work for the first case. Maybe function contracts do not affect smart casting yet.

1 Like

You could write this instead:

fun foo(value: SuperType) {
	value as SealedType
	when (value) {
		is SubType1 -> ...
		is SubType2 -> ...
	}
}

Still I would agree that smart-casting could work a little better there.

Yes and in fact, this is a workaround I’m considering using. It is a little cryptic though. Unfortunately, “proper” solutions are pretty cumbersome:

val value2 = value as SealedType
when (value2) { ... }

Or:

if (value !is SealedType) {
    error("something")
}
when (value) { ... }

Just remove the as SealedType from the when part.
This works:

fun foo(value: SuperType) {
    when (value) {
        is SubType1 -> ... // value is **SubType1**
        is SubType2 -> ... // value is **SubType2**
    }
}

If you need the sealed type exhaustive when, then this is the best I could come up with:

fun foo(value: SuperType) {
    when (val value = value as SealedType) {
        is SubType1 -> println(value) // value is SubType1
        is SubType2 -> println(value) // value is SubType2
    }
}

and with this code you get a Name shadowed: value warning. of course you can use another name for the second val.

1 Like

Yes, I mean exhaustive when and I know we can define a new variable. I consider this a workaround and a step back, because smart casts were invented specifically to avoid reassigning values to new variables for casting.

Thank you for a suggestion.

1 Like

I see your point but actually it is not that easy. It is not just a smart cast problem. when statement is smart casting the parameter it gets, but then you don’t have a way to refer that parameter whenever you use an expression there.

The solution comes to my mind is to be able to access to the parameter of when with a keyword like it. then instead of using value you can use it on the right hand side of any when condition.

I see this problem from a different perspective. I don’t want to access it of when. Normally, when (val value = <expression>) is perfectly fine for me.

For me this is more related to the problem of double casting or casting of as expressions. It is not at all limited to when, see this example:

fun foo(value: SuperType) {
    if (value as MiddleType is SubType) {
        // value is MiddleType, could be: SubType
    }

    // value is MiddleType
}

interface SuperType
interface MiddleType : SuperType
interface SubType : MiddleType

I assume the compiler understands that it can smart cast the expression value as MiddleType to SubType. But it does not understand that by casting this expression it actually casts value itself. I think this case could/should be supported, so value would be smart cast to SubType inside if and to MiddleType outside of if.

Of course, above code does not make too much sense. Why would anybody cast to MiddleType just to immediately cast to SubType? My earlier example with when was to provide a real use case where such double casting actually makes sense.

There is an issue for this: KT-11727. Was just marked as obsolete though, not quite sure why… maybe it’s not a problem in the upcoming K2 compiler but that’s just a guess

1 Like