Kotlin Sealed Class: Complete Guide to Closed Hierarchies and Exhaustive When
A Kotlin sealed class restricts inheritance to a known set of subclasses declared in the same file or package, enabling the compiler to enforce exhaustive when expressions without requiring an else branch.
A Kotlin sealed class provides compile-time safety for closed type hierarchies, making it ideal for representing restricted sets of states or outcomes. According to the JetBrains/kotlin repository source code, the compiler enforces these constraints through dedicated checkers that verify subclass placement and visibility at compile time. This feature bridges the gap between enums and open classes, offering algebraic data type capabilities while maintaining full interoperability with Java.
What Is a Kotlin Sealed Class?
A sealed class is an abstract class with restricted inheritance. Unlike open classes, which permit unknown subclasses at compile time, a sealed class requires all direct subclasses to be defined in the same source file (before Kotlin 1.5) or within the same package (as enforced since Kotlin 1.5). This closed hierarchy allows the Kotlin compiler to perform static analysis that guarantees all possible subtypes are known explicitly.
Key Characteristics
- Closed hierarchy: Subclasses must reside in the same compilation unit or package, preventing external extensions that could break exhaustive pattern matching.
- Exhaustive when: The compiler forces when expressions on a sealed type to cover every subclass, eliminating the need for a default else branch.
- Type-safe polymorphism: Each subclass can carry distinct state and behavior while sharing a common super-type, enabling precise domain modeling.
- Stateful variants: Unlike enums, sealed class subclasses can have their own properties and methods, allowing richer data representation.
Main Use Cases for Kotlin Sealed Classes
Algebraic Data Types and Domain Modeling
Sealed classes excel at modeling algebraic data types (ADTs) where only a finite set of variants exist. This pattern effectively represents restricted domain concepts such as AuthenticationResult.Success versus AuthenticationResult.Failure, where each variant carries different payloads—token strings versus error codes—while maintaining complete type safety.
Exhaustive When Expressions
The compiler leverages the closed nature of sealed classes to enable exhaustive when expressions. When you match against a sealed type, the compiler verifies that every subclass has a corresponding branch via SealedClassInheritorsProvider. If you add a new subclass later, the compiler immediately flags any now-incomplete when expressions, preventing runtime errors. This proves invaluable in UI state management and reducer functions where unhandled cases would cause crashes.
Replacing Enums with Rich Data
While enums constrain each value to a singleton instance with uniform properties, sealed class subclasses can encapsulate varying amounts of data. This makes sealed classes superior for scenarios where variants require different constructor parameters or methods. You gain the type safety of exhaustive matching without the data homogeneity restrictions of enums.
Implementation in the Kotlin Compiler
The sealed class semantics are enforced through several specialized checkers in the JetBrains/kotlin repository:
SealedClassInheritorsProvider.kt(core/descriptors/src/org/jetbrains/kotlin/resolve/): Resolves all inheritors of a sealed class during compilation, providing the infrastructure for exhaustive checking.SealedInheritorInSamePackageChecker.kt(compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/): Validates that subclasses of a sealed class exist within the same package, a relaxation introduced in Kotlin 1.5.SealedInheritorInSameModuleChecker.kt(compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/): Ensures subclasses belong to the same compilation module as the sealed parent.SealedInterfaceAllowedChecker.kt(compiler/frontend/src/org/jetbrains/kotlin/resolve/checkers/): Validates correct usage patterns specific to sealed interfaces, which carry slightly different constraints than sealed classes.
These components work together to guarantee that the hierarchy remains closed and that pattern matching can be proven exhaustive at compile time.
Code Examples
The following example demonstrates a sealed class representing network request states:
sealed class NetworkResult {
data class Success(val payload: String) : NetworkResult()
data class Error(val exception: Throwable) : NetworkResult()
object Loading : NetworkResult()
}
// Exhaustive when - no else needed
fun handle(result: NetworkResult) = when (result) {
is NetworkResult.Success -> println("Got data: ${result.payload}")
is NetworkResult.Error -> println("Failed: ${result.exception}")
NetworkResult.Loading -> println("Loading…")
}
For sealed interfaces (available since Kotlin 1.5), the pattern supports more flexible inheritance:
sealed interface Shape {
data class Circle(val radius: Double) : Shape
data class Rectangle(val width: Double, val height: Double) : Shape
}
fun area(s: Shape): Double = when (s) {
is Shape.Circle -> Math.PI * s.radius * s.radius
is Shape.Rectangle -> s.width * s.height
}
Both examples compile only when all subtypes are handled, demonstrating the compiler's exhaustive checking as implemented in the resolution pipeline.
Summary
- A Kotlin sealed class restricts subclassing to known types declared in the same file or package, creating a closed type hierarchy.
- The compiler enables exhaustive when expressions that require no else branch, catching unhandled cases at compile time.
- Sealed classes support rich algebraic data types where each variant can carry distinct data, unlike enums.
- The implementation relies on
SealedClassInheritorsProviderand package-level checkers in the JetBrains/kotlin compiler to enforce these constraints.
Frequently Asked Questions
What is the difference between a sealed class and an enum in Kotlin?
An enum defines a fixed set of singleton instances with uniform properties, while a sealed class defines a restricted hierarchy where each subclass can have its own constructor parameters, methods, and state. Sealed classes provide the exhaustive matching benefits of enums without constraining data uniformity.
Can sealed class subclasses be defined in different files?
Since Kotlin 1.5, subclasses can reside in the same package as the sealed class but must still belong to the same compilation module. The SealedInheritorInSamePackageChecker and SealedInheritorInSameModuleChecker in the compiler enforce these boundaries; prior to 1.5, subclasses had to appear in the same source file as the parent.
Why do when expressions on sealed classes not require an else branch?
Because the compiler knows every possible subtype at compile time via SealedClassInheritorsProvider, it can verify that all branches are covered. If you add a new subclass, the compiler immediately flags incomplete when expressions as errors, maintaining exhaustive match guarantees without a default case.
How do sealed interfaces differ from sealed classes in Kotlin?
Sealed interfaces (introduced in Kotlin 1.5) allow a type to participate in multiple inheritance hierarchies while maintaining sealed constraints. Unlike sealed classes, they cannot maintain state (no constructor parameters), and they are validated by SealedInterfaceAllowedChecker rather than inheritor checkers designed for classes.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →