Sealed Class vs Sealed Interface vs Enum in Kotlin

Abhinay Gupta
6 min readMar 20, 2024
Source: Guide to using sealed classes in Kotlin — LogRocket Blog

Why do we require sealed classes/interfaces?

In Kotlin, classes and interfaces serve not only as containers for operations or data but also facilitate the representation of hierarchies through polymorphism. For example, consider a scenario where you make a network request. The outcome of this request can either be successful, resulting in the receipt of the requested data, or it can fail, providing information about the encountered error. These two potential outcomes can be depicted using two classes that implement an interface or abstract class:

interface Result
class Success(val data: String) : Result
class Error(val message: String, val statusCode : Int) : Result
abstract class Result
class Success(val data: String) : Result()
class Error(val message: String, val statusCode : Int) : Result()

When function returns the Result object, we know it can either be Success or Error.

val result: Result = getSomeData()
when (result) {
is Success -> handleSuccess(result.data)
is Error -> handleError(result.message, result.statusCode)
}

The issue arises when employing a standard interface or abstract class, as there is no assurance that all defined subclasses necessarily qualify as subtypes of this interface or abstract class. It’s conceivable for someone to define an additional class and have it implement Result. Alternatively, they might implement an object expression that also fulfills the requirements of Result.

A hierarchy wherein the subclasses are not predetermined is referred to as a non-restricted hierarchy. In the case of Result, we opt for a restricted hierarchy, achieved by employing a sealed modifier preceding a class or an interface.

sealed interface Result {
class Success(val data: String) : Result
class Error(val message: String, val statusCode : Int) : Result
}

// or

sealed class Result {
class Success(val data: String) : Result()
class Error(val message: String, val statusCode : Int) : Result()
}

Certain prerequisites must be fulfilled by all children of a sealed class or interface:

  • We don’t need to use the abstract modifier as the sealed modifier makes the class abstract already.
  • They must be declared within the same package and module as the sealed class or interface.
  • They cannot be local or defined using an object expression.

Utilizing the sealed modifier grants you control over the subclasses that a class or interface can have. It prevents clients of your library or module from adding their own direct subclasses. It ensures that no one can surreptitiously introduce a local class or object expression that extends a sealed class or interface. Kotlin has enforced this restriction, thereby confining the hierarchy of subclasses.

Sealed Class

A sealed class is a class that allows subclassing, but solely within the same file where it is defined. Consequently, a sealed class cannot be instantiated directly and cannot possess any subclasses outside of its declaring file. This feature is typically employed to delineate a constrained hierarchy of classes.

Here is an example of a sealed class:

enum class ErrorSeverity { MINOR, MAJOR, CRITICAL }

sealed class Error(val severity: ErrorSeverity) {
class FileReadError(val file: String): Error(ErrorSeverity.MAJOR)
class DatabaseError(val source: String): Error(ErrorSeverity.CRITICAL)
object RuntimeError : Error(ErrorSeverity.CRITICAL)
// Additional error types can be added here
}

// Function to log errors
fun log(e: Error) = when(e) {
is Error.FileReadError -> println("Error while reading file ${e.file}")
is Error.DatabaseError -> println("Error while reading from database ${e.source}")
Error.RuntimeError -> println("Runtime error")
// No `else` clause is required because all the cases are covered
}
sealed class HttpError {
data class Unauthorized(val reason: String): HttpError()
object NotFound: HttpError()
}

Here the sealed class Error have the constructor in the first case, but HttpError is without constructor in the second case.

  • Sealed classes can include constructors with parameters, whereas sealed interfaces cannot.
  • Sealed classes are capable of containing abstract methods and properties, unlike sealed interfaces, which can only contain abstract methods.
  • Sealed classes can be extended by classes, objects, and other sealed classes, whereas sealed interfaces can only be implemented by classes and objects.
  • Sealed classes are frequently utilized alongside when expressions to deliver exhaustive pattern matching.

Sealed classes and when expressions

sealed class Result<out Success>{
class Success<V>(val data: Success) : Response<Success>()
class Error(val errorMessage: String) : Response<Nothing>()
}


fun handle(response: Result<String>) {
val text = when (response) {
is Success -> “Success with ${response.data}”
is Error -> “Error”
// else is not needed here
}
}

Having a finite set of types as an argument empowers us to create an exhaustive when expression with a branch for each conceivable value. When dealing with sealed classes or interfaces, this entails performing is checks for every possible subtype.

Moreover, IntelliJ provides automatic suggestions for adding the remaining branches. This feature greatly enhances the convenience of sealed classes when we aim to encompass all potential types.

Sealed Interface

A sealed interface shares similarities with a sealed class, yet it serves to define a limited set of interfaces rather than classes. Just like sealed classes, sealed interfaces confine the possible subtypes to a finite set specified within the same file as the sealed interface.

Here is an example of a sealed interface:

sealed interface HttpError {
data class Unauthorized(val reason: String): HttpError
object NotFound: HttpError
}
  • Sealed interfaces lack the capability to include constructors with parameters, though they can incorporate properties.
  • They exclusively permit abstract methods, yet they are also capable of featuring default implementations for those methods.
  • Sealed interfaces are implementable by classes and objects, but they cannot be extended by other interfaces or sealed interfaces.
  • They are commonly employed as a means to delineate a cohesive set of functionalities that can be implemented by various classes.

Sealed Interface vs Sealed Class

The two concepts bear striking resemblance, albeit sealed interfaces offer limited functionality compared to sealed classes.

In essence, sealed classes allow the abstract portion of the hierarchy (i.e., the parent class) to possess state. For instance, in the case of HttpError, since it can accept a constructor argument, one can create a function within HttpError that operates on that argument.

On the contrary, sealed interfaces do not permit the parent to have state. Although you can define default functions at the interface level, these functions cannot reference any aspect of the concrete implementations.

In my experience, employing parent or abstract classes with state can introduce readability and reasoning challenges, potentially leading to bugs. Consequently, I tend to avoid implementation inheritance whenever feasible. Therefore, I prefer utilizing sealed interfaces unless there is an absolute necessity to utilize a sealed class.

Enum

Enum classes are the go-to option for representing a collection of well-defined, fixed values. They offer several advantages:

  • Restricted Values: Enum classes define a set of named constants that cannot be extended beyond the initial definition. This guarantees type safety and prevents unexpected values from creeping in, leading to more robust code.
  • Valuable Properties and Methods: You can define additional properties and methods within enum classes, enhancing their functionality beyond simple constants. This allows for a more versatile representation of fixed-value types.
  • Automatic Conversions: Enum classes provide implicit conversions between their string representation and their corresponding enum value. This simplifies usage and improves code readability.

Here’s an example of an enum class representing the days of the week:

enum class DayOfWeek { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY }

Enum vs Sealed Class

Enum classes are employed to depict a predetermined collection of values, while sealed classes or interfaces portray a selection of potential subtypes that can be crafted using class or object declarations. This distinction is noteworthy: a class transcends a mere value, as it can yield multiple instances and serve as a repository for data. Consider the example of Response: if it were an enum class, it would be incapable of encapsulating both value and error. Conversely, sealed subclasses have the flexibility to retain distinct sets of data, whereas an enum solely represents a predefined array of values.

References:

Philipp Lackner: https://www.youtube.com/watch?v=kLJRZpRhX1o

Kotlin Essentials : https://kt.academy/article/kfde-sealed

Kotlin Documentation: https://kotlinlang.org/docs/sealed-classes.html

https://kotlinlang.org/docs/enum-classes.html#working-with-enum-constants

Medium: https://medium.com/@manuchekhrdev/sealed-class-vs-sealed-interface-in-kotlin-47222335040a

--

--