Sealed Classes and Interfaces
Prerequisites: Enum classes, Data classes
Enums work when every option is a single value. When each option needs to carry different data, use a sealed class or interface: a closed family of types that the compiler knows completely.
Example: modeling results
sealed interface PaymentResult {
data class Success(val transactionId: String) : PaymentResult
data class Declined(val reason: String) : PaymentResult
data object Pending : PaymentResult
}
fun message(result: PaymentResult): String = when (result) {
is PaymentResult.Success -> "Paid (ref ${result.transactionId})"
is PaymentResult.Declined -> "Declined: ${result.reason}"
PaymentResult.Pending -> "Waiting for the bank..."
}
fun main() {
println(message(PaymentResult.Success("TX-1042")))
println(message(PaymentResult.Declined("insufficient funds")))
println(message(PaymentResult.Pending))
}
Expected output
Paid (ref TX-1042)
Declined: insufficient funds
Waiting for the bank...
- All subtypes of a sealed type must be declared in the same package and module, so the compiler knows the complete list.
- That makes
whenexhaustive with noelse. Add a new subtype, and everywhenthat doesn't handle it becomes a compile error — exactly where your attention is needed. is PaymentResult.Successsmart-castsresult, soresult.transactionIdworks in that branch.
Making impossible states impossible
Compare a screen state modeled with separate nullable fields — where
data and error could accidentally both be set — with a sealed type:
sealed interface ScreenState {
data object Loading : ScreenState
data class Loaded(val items: List<String>) : ScreenState
data class Failed(val message: String) : ScreenState
}
fun main() {
val state: ScreenState = ScreenState.Loaded(listOf("Ada", "Linus"))
if (state is ScreenState.Loaded) {
println("Showing ${state.items.size} items")
}
println(state)
}
Showing 2 items
Loaded(items=[Ada, Linus])
This pattern is everywhere in Android apps, where a screen's state is exposed as exactly one of these cases.
Sealed class or sealed interface?
Prefer a sealed interface. Use a sealed class when the subtypes need to share state or constructor parameters.
Practice
Define sealed interface Shape with Circle(radius: Double) and
Rectangle(width: Double, height: Double), and an area function using
when. Print both areas with two decimal places for a circle of radius
1 and a 2 by 3 rectangle.
Show solution
sealed interface Shape {
data class Circle(val radius: Double) : Shape
data class Rectangle(val width: Double, val height: Double) : Shape
}
fun area(shape: Shape): Double = when (shape) {
is Shape.Circle -> Math.PI * shape.radius * shape.radius
is Shape.Rectangle -> shape.width * shape.height
}
fun main() {
println("%.2f".format(area(Shape.Circle(1.0))))
println("%.2f".format(area(Shape.Rectangle(2.0, 3.0))))
}
3.14
6.00