Sealed Classes & Enums

Reviewed & published by Brayan K

Kotlin is a modern, concise language that lets you model a fixed set of possibilities precisely — enums for constant values and sealed classes for restricted hierarchies where each case carries its own data.

Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

By the end of this lesson you'll define enums with data and methods, build sealed class hierarchies, and handle them exhaustively with when.

What You'll Learn in This Lesson

1️⃣ Enums

An enum class defines a fixed set of named constants. Each constant can carry data (passed to a constructor), and the enum can have shared properties and methods. You can iterate all values and use name and ordinal.

// An enum is a fixed set of named constants.
enum class Direction {
    NORTH, SOUTH, EAST, WEST
}

// Enums can hold data and define methods.
enum class Planet(val gravity: Double) {
    EARTH(9.8),
    MARS(3.7),
    MOON(1.6);

    fun weightOf(massKg: Double): Double = massKg * gravity
}

fun main() {
    val d = Direction.NORTH
    println(d)                          // -> NORTH
    println(d.name)                     // -> NORTH (the constant's name)
    println(d.ordinal)                  // -> 0 (its position)

    // Iterate every value:
    for (p in Planet.values()) {
        println("${p.name}: ${p.weightOf(10.0)} N")
    }

    // when over an enum is exhaustive — no else needed:
    val label = when (d) {
        Direction.NORTH -> "up"
        Direction.SOUTH -> "down"
        Direction.EAST -> "right"
        Direction.WEST -> "left"
    }
    println(label)                      // -> up
}

Because an enum's cases are fixed, a when over it is exhaustive — the compiler knows you covered every direction, so no else is needed.

2️⃣ Sealed Classes

A sealed class has a closed set of subclasses, all known to the compiler. Unlike an enum, each case can carry different data — making sealed classes ideal for modelling outcomes like success, error, and loading.

// A SEALED class has a fixed set of subclasses, all known to the compiler.
// Unlike an enum, each case can carry DIFFERENT data.
sealed class Result {
    data class Success(val data: String) : Result()
    data class Error(val message: String) : Result()
    object Loading : Result()           // a single shared instance
}

fun handle(result: Result): String = when (result) {
    is Result.Success -> "Got: ${result.data}"   // smart-cast to Success
    is Result.Error -> "Failed: ${result.message}"
    Result.Loading -> "Please wait..."
    // No else needed — the compiler knows all cases are covered.
}

fun main() {
    println(handle(Result.Success("user data")))
    println(handle(Result.Error("not found")))
    println(handle(Result.Loading))
}

The when in handle needs no else, and each is branch smart-casts the result so you can read its specific data directly.

Your turn. Replace the TODO, then run and compare.

// 🎯 YOUR TURN — replace each TODO then run main()

enum class TrafficLight { RED, YELLOW, GREEN }

fun action(light: TrafficLight): String = when (light) {
    TrafficLight.RED -> "Stop"
    TrafficLight.YELLOW -> "Slow"
    TrafficLight.GREEN -> "TODO"        // 👉 replace TODO with "Go"
}

fun main() {
    for (l in TrafficLight.values()) {
        println("${l.name}: ${action(l)}")
    }
    // ✅ Expected:
    //    RED: Stop
    //    YELLOW: Slow
    //    GREEN: Go
}

Mini-Challenge: A Payment Outcome

Model three different outcomes with a sealed class and handle them exhaustively.

// 🎯 MINI-CHALLENGE: model a payment outcome.
// Save as Payment.kt and run main().
//
// 1. Define sealed class Payment with subclasses:
//    - data class Paid(val amount: Int) : Payment()
//    - data class Declined(val reason: String) : Payment()
//    - object Pending : Payment()
// 2. Write fun describe(p: Payment): String using a 'when' over the
//    sealed type (no else): Paid -> "Paid <amount>",
//    Declined -> "Declined: <reason>", Pending -> "Pending".
// 3. In main(), print describe() for one of each.
//
// ✅ Example output:
//    Paid 50
//    Declined: low funds
//    Pending

fun main() {
    // your code here
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Sealed & Enums

ConceptKotlin Syntax
Enumenum class Dir { NORTH, SOUTH }
Enum with dataEARTH(9.8)
All valuesDir.values()
Sealed classsealed class Result
Data casedata class Ok(...) : Result()
Singleton caseobject Loading : Result()

🎉 Lesson Complete!

Practice quiz

What is an enum class?

  • A class with no methods
  • A class that cannot be instantiated
  • A fixed set of named constants
  • A type alias

Answer: A fixed set of named constants. An enum class defines a fixed set of named constants.

What does the ordinal property of an enum constant give you?

  • Its zero-based position in the declaration order
  • Its name as text
  • A random id
  • Its constructor argument

Answer: Its zero-based position in the declaration order. ordinal is the zero-based position; the first constant has ordinal 0.

When is a when over an enum exhaustive without an else?

  • Never, an else is always required
  • Only with more than three constants
  • Only when used as a statement
  • When it covers every constant of the enum

Answer: When it covers every constant of the enum. The compiler knows all constants, so covering them all makes when exhaustive.

How does a sealed class differ from an enum?

  • It cannot be used in when
  • Its cases can each carry different data
  • It has no subclasses
  • It is always a singleton

Answer: Its cases can each carry different data. Sealed-class cases can each carry different data, unlike enum constants.

Why does object Loading : Result() use object instead of a data class?

  • Loading carries no data, so a single shared instance fits
  • Loading needs many instances
  • data classes cannot extend sealed classes
  • object is faster to type

Answer: Loading carries no data, so a single shared instance fits. A no-data case is best modeled as an object singleton.

In when, what does an is Result.Success branch let you do with the value?

  • Nothing extra
  • Only print it
  • Smart-cast it to Success and read its data directly
  • Cast it manually with as

Answer: Smart-cast it to Success and read its data directly. An is branch smart-casts the value to the specific subtype.

Where must a sealed class's subclasses be declared?

  • Anywhere in the project
  • In the same file or module so the compiler sees them all
  • Only inside a companion object
  • In a separate library

Answer: In the same file or module so the compiler sees them all. Sealed subclasses must be in the same file/module for exhaustiveness checking.

Can an enum constant carry constructor data?

  • No, enums only hold names
  • Only the first constant can
  • Only via inheritance
  • Yes, like EARTH(9.8) passing a gravity value

Answer: Yes, like EARTH(9.8) passing a gravity value. Enum constants can take constructor arguments, such as Planet(9.8).

What separates enum constants from member methods in the body?

  • A comma
  • A semicolon after the last constant
  • A blank line
  • Nothing

Answer: A semicolon after the last constant. When an enum also defines methods, a semicolon separates constants from members.

Which is the modern alternative to values() in newer Kotlin?

  • allValues()
  • constants()
  • entries
  • list()

Answer: entries. Newer Kotlin offers EnumName.entries as a modern alternative to values().

Continue this course

Frequently asked questions

When should I use an enum versus a sealed class?

Use an enum when you have a fixed set of constant values that are essentially the same shape — like directions, days of the week, or status flags — optionally each carrying the same kind of data. Use a sealed class when your fixed set of cases need to carry different data per case, like a Result that is either Success(data), Error(message), or Loading. Enums are a list of singletons; sealed classes are a restricted hierarchy where each subclass can have its own properties.

Why don't I need an else branch in a when over a sealed class or enum?

Because the compiler knows the complete set of possible cases. An enum has a finite list of constants, and a sealed class's subclasses must all be declared in the same file (or module), so the compiler can verify your when covers every one. When it is exhaustive, no else is required — and if you later add a new case, the compiler will flag every when that no longer covers all options, helping you update them.

What is the 'object' keyword doing inside a sealed class?

object declares a singleton — a single, shared instance with no constructor parameters. Inside a sealed class it is ideal for cases that carry no data, like Loading or Pending: there's only ever one Loading, so a singleton is exactly right. Cases that do carry data are usually data classes instead, so each instance can hold its own values.

Can enums have behaviour, not just values?

Yes. Enum constants can take constructor arguments (like Planet's gravity) and the enum class can declare properties and methods that all constants share, such as weightOf(). You can even give individual constants their own overridden method implementations using an anonymous body. This makes enums far more powerful than plain integer constants in older languages.