In the previous tutorial, you learned about scope functions. Now let’s learn about three important types in Kotlin: enum classes, sealed classes, and value classes. Each one solves a different problem, and knowing when to use which will make your code better.
In this tutorial, you will learn:
- Enum classes — fixed set of constants
- Sealed classes — restricted hierarchies with different data
- Sealed interfaces — multiple inheritance with sealed types
- Value classes — type safety without runtime overhead
- When to use which
Enum Classes
An enum class defines a fixed set of constants. Every value is known at compile time.
Basic Enum
enum class Direction {
NORTH, SOUTH, EAST, WEST
}
val direction = Direction.NORTH
println(direction) // NORTH
Enum in when
When you use all enum values in a when expression, you do not need an else branch:
val message = when (direction) {
Direction.NORTH -> "Going up"
Direction.SOUTH -> "Going down"
Direction.EAST -> "Going right"
Direction.WEST -> "Going left"
}
If you add a new enum value later, the compiler will warn you about every when that does not handle it. This is a big advantage.
Enum with Properties and Methods
Enums can have properties, constructors, and methods:
enum class Planet(val mass: Double, val radius: Double) {
MERCURY(3.303e+23, 2.4397e6),
VENUS(4.869e+24, 6.0518e6),
EARTH(5.976e+24, 6.37814e6),
MARS(6.421e+23, 3.3972e6);
fun surfaceGravity(): Double {
val G = 6.67300E-11
return G * mass / (radius * radius)
}
}
println(Planet.EARTH.mass) // 5.976E24
println(Planet.EARTH.surfaceGravity()) // ~9.8
Enum with Interface
Enums can implement interfaces:
enum class LogLevel(val severity: Int) : Printable {
DEBUG(0) { override fun display() = "DEBUG" },
INFO(1) { override fun display() = "INFO" },
WARNING(2) { override fun display() = "WARNING" },
ERROR(3) { override fun display() = "ERROR" };
fun isHighSeverity(): Boolean = severity >= 2
}
println(LogLevel.ERROR.isHighSeverity()) // true
println(LogLevel.DEBUG.display()) // DEBUG
Enum as State Machine
Enums are great for state machines. You can define allowed transitions:
enum class OrderStatus {
PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED;
fun canTransitionTo(next: OrderStatus): Boolean {
return when (this) {
PENDING -> next == CONFIRMED || next == CANCELLED
CONFIRMED -> next == SHIPPED || next == CANCELLED
SHIPPED -> next == DELIVERED
DELIVERED -> false
CANCELLED -> false
}
}
}
val status = OrderStatus.PENDING
println(status.canTransitionTo(OrderStatus.CONFIRMED)) // true
println(status.canTransitionTo(OrderStatus.DELIVERED)) // false
Useful Enum Properties
Every enum has built-in properties:
// Get all values
println(Direction.entries) // [NORTH, SOUTH, EAST, WEST]
// Get by name
val dir = Direction.valueOf("EAST")
println(dir) // EAST
// Ordinal (position starting from 0)
println(Direction.NORTH.ordinal) // 0
println(Direction.EAST.ordinal) // 2
// Name
println(Direction.NORTH.name) // NORTH
Sealed Classes
A sealed class is like an enum, but each subclass can hold different data. All subclasses must be defined in the same file.
Basic Sealed Class
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String, val code: Int) : Result()
data object Loading : Result()
}
Each subclass has different properties. Success has data, Error has message and code, and Loading has nothing.
Exhaustive when
Like enums, sealed classes work with exhaustive when:
fun handleResult(result: Result): String {
return when (result) {
is Result.Success -> "Success: ${result.data}"
is Result.Error -> "Error ${result.code}: ${result.message}"
is Result.Loading -> "Loading..."
}
}
println(handleResult(Result.Success("Data loaded")))
// Success: Data loaded
println(handleResult(Result.Error("Not found", 404)))
// Error 404: Not found
UI State Example
Sealed classes are perfect for representing UI state:
sealed class UiState {
data object Idle : UiState()
data object Loading : UiState()
data class Content(val items: List<String>) : UiState()
data class Error(val message: String) : UiState()
}
fun renderUi(state: UiState): String {
return when (state) {
is UiState.Idle -> "Waiting..."
is UiState.Loading -> "Loading..."
is UiState.Content -> "Showing ${state.items.size} items"
is UiState.Error -> "Error: ${state.message}"
}
}
Payment Method Example
Each payment type needs different data:
sealed class PaymentMethod {
data class CreditCard(val number: String, val expiry: String) : PaymentMethod()
data class BankTransfer(val iban: String) : PaymentMethod()
data class PayPal(val email: String) : PaymentMethod()
data object Cash : PaymentMethod()
}
fun processPayment(method: PaymentMethod): String {
return when (method) {
is PaymentMethod.CreditCard ->
"Charging card ending in ${method.number.takeLast(4)}"
is PaymentMethod.BankTransfer ->
"Transferring to IBAN: ${method.iban}"
is PaymentMethod.PayPal ->
"Sending to ${method.email}"
is PaymentMethod.Cash ->
"Paying with cash"
}
}
Generic Sealed Class
Sealed classes can use generics:
sealed class NetworkResponse<out T> {
data class Success<T>(val data: T) : NetworkResponse<T>()
data class Error(val message: String) : NetworkResponse<Nothing>()
data object Loading : NetworkResponse<Nothing>()
}
Sealed Interfaces
Sealed interfaces are like sealed classes but allow multiple inheritance. A class can implement multiple sealed interfaces.
sealed interface Shape {
fun area(): Double
}
data class Circle(val radius: Double) : Shape {
override fun area(): Double = Math.PI * radius * radius
}
data class Rectangle(val width: Double, val height: Double) : Shape {
override fun area(): Double = width * height
}
data class Triangle(val base: Double, val height: Double) : Shape {
override fun area(): Double = 0.5 * base * height
}
Use them the same way as sealed classes:
val shapes: List<Shape> = listOf(
Circle(5.0),
Rectangle(4.0, 6.0),
Triangle(3.0, 8.0)
)
for (shape in shapes) {
val description = when (shape) {
is Circle -> "Circle r=${shape.radius}"
is Rectangle -> "Rectangle ${shape.width}x${shape.height}"
is Triangle -> "Triangle b=${shape.base} h=${shape.height}"
}
println("$description -> area = ${"%.2f".format(shape.area())}")
}
Value Classes
A value class wraps a single value without runtime overhead. The compiler replaces the class with the wrapped value at runtime. Use @JvmInline for JVM compatibility.
Basic Value Class
@JvmInline
value class Email(val value: String) {
init {
require(value.contains("@")) { "Invalid email: $value" }
}
fun domain(): String = value.substringAfter("@")
}
val email = Email("alex@mail.com")
println(email.value) // alex@mail.com
println(email.domain()) // mail.com
At runtime, this is just a String. No extra object is created.
Type Safety with Value Classes
Value classes prevent mixing up types that have the same underlying type:
@JvmInline
value class UserId(val value: Long) {
init { require(value > 0) { "UserId must be positive" } }
}
@JvmInline
value class Password(val value: String) {
init { require(value.length >= 8) { "Password must be at least 8 chars" } }
val strength: String
get() = when {
value.length >= 16 -> "Strong"
value.length >= 12 -> "Medium"
else -> "Weak"
}
}
@JvmInline
value class Meters(val value: Double) {
fun toKilometers(): Double = value / 1000.0
fun toCentimeters(): Double = value * 100.0
}
Usage:
fun findUser(id: UserId): String = "User #${id.value}"
fun sendEmail(to: Email, subject: String): String =
"Email sent to ${to.value}: $subject"
val userId = UserId(42)
println(findUser(userId)) // User #42
val email = Email("alex@mail.com")
println(sendEmail(email, "Welcome!"))
Without value classes, you might accidentally pass a userId where an orderId is expected. Both are Long values, so the compiler would not catch the mistake. Value classes solve this problem.
Value Class Properties
Value classes can have computed properties but cannot store extra state:
val password = Password("mySecurePassword123")
println(password.strength) // Strong
val distance = Meters(1500.0)
println(distance.toKilometers()) // 1.5
Enum vs Sealed Class — Key Differences
Here is a detailed comparison:
| Feature | Enum | Sealed Class |
|---|---|---|
| Instances | Fixed at compile time | Created at runtime |
| Data | Same properties for all | Different properties per subclass |
| Hierarchy | Flat | Hierarchical |
entries | Yes | No |
valueOf() | Yes | No |
| Ordinal | Yes | No |
| Serializable | Automatic | Manual |
The simplest way to decide: if every value has the same structure (like Direction.NORTH, Direction.SOUTH), use an enum. If different values need different data (like Success(data) vs Error(message, code)), use a sealed class.
Combining Sealed and Enum
You can use enums inside sealed classes:
enum class ErrorCode { NOT_FOUND, UNAUTHORIZED, SERVER_ERROR }
sealed class AppResult<out T> {
data class Success<T>(val data: T) : AppResult<T>()
data class Error(val code: ErrorCode, val message: String) : AppResult<Nothing>()
data object Loading : AppResult<Nothing>()
}
fun handleError(error: AppResult.Error) {
when (error.code) {
ErrorCode.NOT_FOUND -> println("Not found: ${error.message}")
ErrorCode.UNAUTHORIZED -> println("Auth error: ${error.message}")
ErrorCode.SERVER_ERROR -> println("Server error: ${error.message}")
}
}
This gives you the best of both worlds: type-safe error codes with flexible data.
Value Class Limitations
Value classes have some restrictions:
- Can only wrap a single value
- Can only have one
valproperty in the primary constructor (initblocks are allowed for validation) - Cannot participate in class hierarchies (no
open,abstract, orsealed) - Cannot have
varproperties (onlyval) - Cannot use
===identity checks reliably (the object may or may not exist at runtime)
Despite these limitations, value classes are very useful for domain modeling. They cost nothing at runtime but give you compile-time type safety.
When to Use Which
| Type | Use when |
|---|---|
| Enum class | Fixed set of constants, all values known at compile time |
| Sealed class | Each subclass holds different data, restricted hierarchy |
| Sealed interface | Like sealed class but need multiple inheritance |
| Value class | Wrap a single value for type safety, zero overhead |
Examples:
- Direction (N, S, E, W) — Enum
- LogLevel (DEBUG, INFO, ERROR) — Enum
- UI state (Loading, Content, Error) — Sealed class
- Network response (Success, Error) — Sealed class
- Shape (Circle, Rectangle, Triangle) — Sealed interface
- Email, UserId, Meters — Value class
Real-World Example: Form Validation
Here is a complete example using sealed classes and value classes together:
@JvmInline
value class EmailAddress(val value: String) {
init { require(value.contains("@")) }
}
@JvmInline
value class Age(val value: Int) {
init { require(value in 0..150) }
}
sealed class ValidationResult {
data object Valid : ValidationResult()
data class Invalid(val errors: List<String>) : ValidationResult()
}
fun validateRegistration(
name: String,
email: String,
age: Int
): ValidationResult {
val errors = mutableListOf<String>()
if (name.isBlank()) errors.add("Name is required")
runCatching { EmailAddress(email) }
.onFailure { errors.add("Invalid email") }
runCatching { Age(age) }
.onFailure { errors.add("Age must be between 0 and 150") }
return if (errors.isEmpty()) ValidationResult.Valid
else ValidationResult.Invalid(errors)
}
when (val result = validateRegistration("Alex", "alex@mail.com", 25)) {
is ValidationResult.Valid -> println("Registration successful!")
is ValidationResult.Invalid -> {
println("Errors:")
result.errors.forEach { println(" - $it") }
}
}
This example shows how sealed classes (for the result) and value classes (for validated types) work together in real code.
Source Code
You can find the complete source code for this tutorial on GitHub:
What’s Next?
In this tutorial, you learned about enum classes, sealed classes, sealed interfaces, and value classes. These are essential tools for modeling your data in Kotlin.
In the next tutorial, you will learn about interfaces, generics, and type constraints. You will see how to write code that works with any type while keeping type safety.
This is part 12 of the Kotlin Tutorial series. Check out Part 11: Scope Functions if you missed it. Need a quick reference? See the Kotlin Cheat Sheet.