Multiple inheritance including a sealed interface

I’m trying to define a type that derives from a sealed interface and also a regular interface. However, I’m unable to find a way of doing this without inadvertently defining a direct subclass of the sealed interface.

For example, suppose I have a Fruit sealed interface:

sealed interface Fruit {
    open class Apple : Fruit
    open class Orange : Fruit
}

Now I want to have fruit with additional properties - say I’m implementing a fruit shop, and the shop’s fruit is branded:

interface Branded {
    val brand: String
}

I can create objects that implement both interfaces:

val brandedApple = object : Fruit.Apple(), Branded {
    override val brand = "FruitsRUs"
}

The problem is: what is the type of brandedApple? If I don’t declare the type explicitly, the compiler knows brandedApple is both a Fruit and a Branded, but AFAIK, Kotlin doesn’t have a way of declaring that a value has more than one type (Intellij’s “Specify type explicitly” intention only offers to declare it as a Fruit).

My first thought was to declare an interface that derives from both parent interfaces:

interface BrandedFruit: Fruit, Branded

The issue with this is that BrandedFruit is treated as a direct subclass of Fruit, so now the compiler requires this kind of thing:

fun processFruit(fruit: Fruit) = when (fruit) {
    is Fruit.Apple -> "I like crunchy apples"
    is Fruit.Orange -> "Oranges are best as juice"
    is BrandedFruit -> TODO()
}

How do I declare a type that extends both Fruit and Branded without that type being a direct subclass of Fruit?

1 Like

Intellij may not be able to refactor it for you but you can still do it yourself.

class BrandedApple: Fruit.Apple(), Branded {
    override val brand = "FruitsRUs"
}

@nickallendev ah, that explains the anonymous type assigned to brandedApple - thanks.

Unfortunately, as you point out, it extends Fruit.Apple(), Branded, rather than Fruit, Branded. Perhaps this isn’t possible, though I don’t understand why it wouldn’t be.

1 Like

What exactly are you trying to do? Your original post is detailed about what you think can and can’t be done, but is pretty vague about what you actually want to do.

Do you need all your Fruit to implement Branded?

sealed interface Fruit : Branded {
    open class Apple : Fruit {
        override val brand = "ApplesOfMyEye"
    }
    open class Orange : Fruit {
        override val brand = "OrangeUGladToSeeMe"
    }
}

Maybe you are looking for generics with multiple type constraints?

fun <T> printBrandedFruitBranch(brandedFruit: T) where T : Fruit, T : Branded {
    println(brandedFruit.brand)
}

Thanks for the help.

You may be onto something with type constraints. I want a type is both a subtype of Branded and an instance of any existing subtype of Fruit, in order that I can treat it polymorphically as both a Fruit and a Branded

Concretely, I want to do this:

fun interface FruitSupplier<T : Fruit> {
    fun pickAFruit(): T
}

val appleTree: FruitSupplier<Fruit> = FruitSupplier { Fruit.Apple() }

val fruitStand: FruitSupplier<TODO> = FruitSupplier { listOf<Fruit>(
    object : Fruit.Apple(), Branded { override val brand = "FruitsRUs" },
    object : Fruit.Orange(), Branded { override val brand = "FruitCorp" }
).random() }

Code that only deals with Fruit should be able to work with appleTree or fruitStand without caring that the objetcts fruitStand returns are both Fruit and Branded.

I don’t know what to put as the type parameter in the declaration of fruitStand, in place of TODO. As you point out, the type constraint T where T : Branded, T : Fruit expresses it precisely, but I don’t know how to work a type constraint into the declaration of fruitStand.

That doesn’t really make sense. Fruit is sealed and yet you seem to want to make arbitrary number of subclasses. Note that sealed does not apply just to direct subclasses, the entire hierarchy is intended to be sealed.

I recommend switching over to seeing how composition can benefit your use case.

Maybe something like:


interface Fruity {
    val fruit: Fruit
}
sealed interface Fruit: Fruity {
    open class Apple : Fruit
    open class Orange : Fruit
    override val fruit get() = this
}

class BrandedFruit(override val fruit: Fruit, brand: String) : Fruity {

fun interface FruitSupplier<T : Fruity> {
    fun pickAFruit(): T
}

It’s also not clear to me that you need a sealed class at all. Are Apple and Orange different in any way other than their type? Do you instead really need something more like:

interface Fruit {
    val fruitType: FruitType
}
interface Branded {
    val brand: String
}
enum class FruitType {
    Apple,
    Orange
}
interface BrandedFruit: Branded, Fruit

And my other suggestion is not to model your types off of what you think they are but rather what you need. If you don’t methods/properties that actually use the Branded interface, then don’t define it, don’t inherit from it.

I guess I should clarify that the Fruit example is just intended to be an illustration.

The most precise way I can think of stating the question is: Can I define a type T such that instances of T can be of any type that is a subclass of both A and B if A is sealed?

Incidentally,

This is contradicted by the documentation on sealed classes:

All direct subclasses of a sealed class are known at compile time.

My tests bear this out - you can create an indirect subclass of a sealed class in another module.

1 Like