Enums and sealed hierarchies

Kotlin · Practical · 12 min

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.

Check yourself

  1. What happens when you add a new subtype to a sealed interface that is matched in a when without else?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: kotlinlang.org/docs/sealed-classes.html

Builds on

Unlocks

Nothing builds on this yet.