How it works
The explanation, with examples you can run.
A closed set of states
sealed interface Payment { data object Pending : Payment data class Paid(val receiptId: String) : Payment data class Failed(val reason: String, val retryable: Boolean) : Payment} fun label(p: Payment): String = when (p) { Payment.Pending -> "waiting" is Payment.Paid -> "paid, receipt ${p.receiptId}" // smart cast to Paid is Payment.Failed if p.retryable -> "failed, try again" // guard condition (Kotlin 2.2) is Payment.Failed -> "failed: ${p.reason}"} fun main() { listOf(Payment.Pending, Payment.Paid("r-1"), Payment.Failed("card declined", true), Payment.Failed("fraud", false)).forEach { println(label(it)) }}- No
elsebranch: thewhenis exhaustive because every subtype is covered. - In
is Payment.Paid ->,pis smart cast, sop.receiptIdworks. data object(Kotlin 1.9) gives a singleton case a readabletoString()(Pending) and a properequals.- Guard conditions (
is Failed if p.retryable ->), stable since Kotlin 2.2, add a condition to a branch without nesting anotherif. A guarded branch doesn't count towards exhaustiveness, so the plainis Failedbranch is still needed.
What exhaustiveness buys you
Add data class Refunded(val amount: Long) : Payment and every when expression over Payment without else stops compiling. That's the safety net: the compiler finds the places to update. An else branch switches it off for good, so leave it out for your own sealed types.
Since Kotlin 1.7, when statements over sealed types, enums and Boolean must be exhaustive too. Kotlin 2.3 made the checks data-flow aware (stable in 2.3): if an earlier if (p is Payment.Pending) return has ruled a case out, a later when doesn't need to list it.
Sealed class or sealed interface
- A sealed class can hold state and a constructor shared by all subclasses; a subclass can extend only one class.
- A sealed interface (Kotlin 1.5) lets a type belong to several closed hierarchies, and lets enums and objects implement it.
Subclasses themselves don't have to be final: a subclass can be open, and then the hierarchy below it is no longer closed. The compiler only knows the direct subtypes.
Typical uses
- UI state:
Loading,Content(items),Error(message), rendered by onewhenin the UI layer. - Results: a domain-specific
Resultwith typed failure cases, instead of throwing. - Commands and events: messages between layers or over a channel, each with its own fields.
On the JVM a sealed class is an abstract class whose constructors can't be called from outside the hierarchy; the closed list of subtypes is recorded in Kotlin's metadata, which is how the compiler checks when across modules.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.