Extending data class from a sealed class in Kotlin

f2prateek picture f2prateek · Jun 7, 2017 · Viewed 12.2k times · Source

I have a set of data classes that share some common fields, So ideally I'd like to declare those in a supertype (Message in this example), and be able to write functions that operate on the supertype if they need access to these common fields (messageId in this example).

fun operate(m: Message) {
  use(m.messageId)
}

I tried to accomplish this by extending my data classes from a sealed class.

Data classes may extend sealed classes, but not I'm not sure how/if they can accept arguments required by the "supertype" sealed class.

  1. Extending a regular class from a sealed class compiles just fine.

    sealed class Message(val messageId: String)
    
    class Track(val event: String, messageId: String): Message(messageId)
    
  2. However, changing it to a data class doesn't compile ("Data class primary constructor must have only property (val/var) parameters.").

    sealed class Message(val messageId: String)
    
    data class Track(val event: String, messageId: String): Message(messageId)
    
  3. Declaring the parameter as a property also doesn't compile ("'messageId' hides member of supertype 'Message' and needs 'override' modifier'").

    sealed class Message(val messageId: String)
    
    data class Track(val event: String, val messageId: String): Message(messageId)
    
  4. Opening the supertype property and overriding it in each of the base classes compiles fine:

    sealed class Message(open val messageId: String)
    
    data class Track(val event: String, override val messageId: String): Message(messageId)
    

Ideally I would like something close to Option 2 - it allows me to combine the best of both worlds.

Otherwise, it seems my options are either handrolling my own data class functionality (copy, hashcode, equals etc) with option 1, or live with a compromise by opening up up the supertype properties with option 4.

Answer

Kiskae picture Kiskae · Jun 7, 2017

Options 3 and 4 would result in the class holding messageId twice. Once in the new class and once in its superclass.

The solution is to declare but not define the variable in the superclass:

sealed class Message {
    abstract val messageId: String
}

data class Track(val event: String, override val messageId: String): Message()

This will make the messageId available on Message, but delegates the storage to whatever implements it.