Sealed types make illegal states unrepresentable and let the compiler check every case is handled.
An enum class defines a fixed set of constants. Each constant can carry properties and the enum can have methods:
enum class Level(val weight: Int) { LOW(1), MEDIUM(5), HIGH(10) }Enums provide name, ordinal, entries (all values, preferred over the older values()), and valueOf("HIGH").
A sealed class or sealed interface restricts its direct subtypes to those declared in the same module and package, so the compiler knows the full list. Unlike enums, each subtype can be a different class with its own data and many instances:
sealed interface Result {
data class Ok(val value: String) : Result
data class Error(val message: String, val code: Int) : Result
data object Loading : Result
}The payoff is an exhaustive `when`: when every subtype is covered, no else branch is needed, and adding a new subtype later produces compile errors at every when that doesn't handle it. This is far safer than an else that silently swallows new cases.
Inside each branch the value is smart-cast, so you can access result.value or result.message directly. Kotlin 2.2 also allows guard conditions: is Result.Error if result.code >= 500 -> ....
Use enums for simple labels, sealed hierarchies for states, events, API responses and domain outcomes.
Example
enum class Level(val weight: Int) { LOW(1), MEDIUM(5), HIGH(10) }
sealed interface Result {
data class Ok(val value: String) : Result
data class Error(val message: String, val code: Int) : Result
data object Loading : Result
}
fun render(r: Result): String = when (r) { // exhaustive, no else
is Result.Ok -> "value: ${r.value}" // smart cast
is Result.Error if r.code >= 500 -> "server error: ${r.message}" // guard (2.2)
is Result.Error -> "error ${r.code}"
Result.Loading -> "loading..."
}
fun main() {
println(Level.entries.map { it.name }) // [LOW, MEDIUM, HIGH]
println(Level.valueOf("HIGH").weight) // 10
println(Level.MEDIUM > Level.LOW) // true (by ordinal)
listOf(Result.Ok("42"), Result.Error("db down", 503), Result.Error("nope", 404), Result.Loading)
.forEach { println(render(it)) }
// value: 42 / server error: db down / error 404 / loading...
}Remember
- enum class for fixed constants with properties; use entries to list them
- Sealed classes/interfaces fix the set of subtypes at compile time
- when over a sealed type is exhaustive without else
- Avoid else on sealed when: you lose compile errors for new cases
Watch out for
- Adding an else branch to a sealed when, so new subtypes are silently unhandled.