Introduction
Sealed classes represent one of Kotlin's most powerful features for creating type-safe hierarchies. A sealed class restricts inheritance to a predefined set of subclasses, all of which are known at compile time. This controlled inheritance model enables developers to build robust type systems where the compiler can verify that all possible cases are handled, eliminating entire categories of runtime errors.
The fundamental value proposition of sealed classes lies in their ability to combine the exhaustive pattern matching capabilities of enums with the flexibility of class hierarchies. While enums constrain you to a fixed set of singleton values, sealed classes allow each subclass to be a full class with its own properties, methods, and inheritance hierarchy. This makes sealed classes ideal for representing complex states, events, and domain models where you need both type safety and extensibility.
When you declare a class as sealed, you establish a closed type hierarchy. No other subclasses may appear outside the module and package within which the sealed class is defined. This restriction provides powerful compile-time guarantees about what types exist in your system, enabling the Kotlin compiler to perform exhaustiveness checks in when expressions and helping IDEs provide accurate autocomplete suggestions.
Why Sealed Classes Matter
The modern software development landscape demands type safety more than ever. As applications grow in complexity, the ability to represent domain concepts accurately becomes crucial. Sealed classes address a fundamental challenge: how to represent a finite but potentially complex set of related types while maintaining compile-time safety.
Consider the alternative approaches and their limitations. Using plain enums works well for simple categorical data but breaks down when you need to associate different data with each case or when you need polymorphic behavior. Using regular class hierarchies provides flexibility but loses compile-time exhaustiveness guarantees. Regular abstract classes allow any class to extend them, meaning you can never be certain you've handled all possible subtypes.
Sealed classes occupy a sweet spot between these extremes. They provide the exhaustiveness guarantees of enums while offering the flexibility of full class hierarchies. Each sealed subclass can be a data class holding specific values, an object representing a singleton state, or even a regular class with complex behavior. This flexibility makes sealed classes applicable across a wide range of scenarios, from simple result types to sophisticated state machines.
For teams building web applications with Kotlin, mastering sealed classes is essential for creating maintainable, type-safe codebases that leverage Kotlin's powerful type system features. These patterns are particularly valuable in API-driven architectures where type-safe communication between services is critical.
Understanding why sealed classes are essential for type-safe Kotlin code
Compile-Time Exhaustiveness
The compiler verifies that all possible subtypes are handled in when expressions, eliminating runtime errors from unhandled cases.
Type-Safe Hierarchies
Restricted inheritance ensures all possible types are known at compile time, enabling powerful static analysis.
Flexible Subtypes
Each subclass can be a data class, object, or regular class with its own properties and behavior.
Domain Modeling
Model complex domains like UI states, API responses, and workflow transitions with precision.
Declaration and Syntax
Basic Sealed Class Declaration
Declaring a sealed class follows the same pattern as other class declarations in Kotlin, with the addition of the sealed modifier. The sealed modifier signals to the compiler that this class forms a closed type hierarchy, and all possible subclasses must be declared within the same file (or same module, depending on Kotlin version and context).
The most basic sealed class declaration creates a type that can be extended only by its declared subclasses. These subclasses can be declared in the same file and must have proper names--they cannot be local or anonymous objects. This naming requirement ensures that each subclass is fully type-checkable and can be referenced unambiguously in when expressions.
// A simple sealed class representing different error types
sealed class IOError {
class FileNotFoundError(val file: String) : IOError()
class PermissionDeniedError(val path: String) : IOError()
class DiskFullError(val volume: String) : IOError()
}
// Using the sealed class
fun handleError(error: IOError) {
when (error) {
is IOError.FileNotFoundError -> println("File not found: ${error.file}")
is IOError.PermissionDeniedError -> println("Permission denied: ${error.path}")
is IOError.DiskFullError -> println("Disk full: ${error.volume}")
}
}
This example demonstrates several key aspects of sealed class usage. First, each subclass extends the sealed parent and can carry its own properties. Second, the when expression can pattern match on each subclass type. Third, the compiler knows that these three classes represent all possible IOError types, so no else branch is required for exhaustiveness.
Sealed Interfaces
Kotlin extends the sealed concept beyond classes to interfaces. Sealed interfaces work similarly to sealed classes but use interface syntax. This allows you to create sealed hierarchies that can be implemented by classes from anywhere, provided those implementations occur within the same module. The key distinction is that sealed interfaces define a contract that other types can fulfill, while sealed classes define a base type that other types extend.
Sealed interfaces prove particularly useful when you want to define a sealed hierarchy that can be implemented by classes you don't control. For example, you might define a sealed interface for API responses that your domain layer understands, while the actual implementation classes live in a data layer module.
// A sealed interface for API results
sealed interface ApiResult<out T> {
data class Success<T>(val data: T) : ApiResult<T>
data class Error(val message: String, val code: Int) : ApiResult<Nothing>
data object Loading : ApiResult<Nothing>
}
// Using sealed interface
fun <T> processResult(result: ApiResult<T>) {
when (result) {
is ApiResult.Success -> println("Got data: ${result.data}")
is ApiResult.Error -> println("Error ${result.code}: ${result.message}")
is ApiResult.Loading -> println("Loading...")
}
}
Combining Sealed Classes and Interfaces
Kotlin allows sophisticated hierarchies combining sealed classes and interfaces. A sealed class can implement sealed interfaces, and interfaces can extend other interfaces within the sealed hierarchy. This flexibility enables you to build complex type systems that precisely model your domain while maintaining the compile-time safety guarantees that sealed types provide.
The ability to mix sealed classes and interfaces becomes valuable in layered architectures where different abstraction levels exist. You might have a top-level sealed interface representing generic results, with sealed classes implementing specific result categories, and each sealed class having its own set of concrete subclasses.
When building custom software solutions, these patterns help create clean, maintainable architectures with strong type safety. Combined with enterprise-grade development practices, sealed classes help prevent entire categories of bugs at compile time.
Constructors and Initialization
Understanding Sealed Class Constructors
A sealed class itself is always abstract and cannot be instantiated directly. However, it may contain or inherit constructors. These constructors serve a different purpose than those in regular classes--they initialize the sealed class for use by its subclasses rather than creating instances of the sealed class itself.
The primary constructor of a sealed class, if declared, typically defines properties that all subclasses will share. This allows you to establish common state or behavior that every subclass inherits. The constructor parameters become part of the sealed class's type signature and are accessible to subclasses through their superclass initialization.
// Sealed class with constructor parameters
sealed class Vehicle(val id: String) {
class Car(id: String, val wheels: Int) : Vehicle(id)
class Motorcycle(id: String, val engineCC: Int) : Vehicle(id)
class Bicycle(id: String, val gearCount: Int) : Vehicle(id)
}
// Usage demonstrates shared property access
fun identifyVehicle(vehicle: Vehicle) {
println("Vehicle ID: ${vehicle.id}") // Available from sealed class
when (vehicle) {
is Vehicle.Car -> println("Car with ${vehicle.wheels} wheels")
is Vehicle.Motorcycle -> println("Motorcycle with ${vehicle.engineCC}cc engine")
is Vehicle.Bicycle -> println("Bicycle with ${vehicle.gearCount} gears")
}
}
Constructor visibility in sealed classes follows specific rules designed to maintain the sealed nature of the hierarchy. By default, sealed class constructors have protected visibility, meaning they are accessible within the sealed class itself and its subclasses. You can also declare constructors as private, which restricts initialization even further. Public and internal constructors are not allowed in sealed classes, as they would contradict the sealed semantics by allowing external instantiation.
Private Constructors for Strict Control
Declaring a private constructor in a sealed class enables even stricter control over instantiation. This approach proves valuable when you want to prevent direct subclass instantiation outside of specific factory methods or companion object patterns. The private constructor ensures that all initialization goes through controlled pathways, which can be useful for implementing singleton patterns, enforcing invariants, or managing complex initialization logic.
sealed class Configuration private constructor(val env: String) {
data object Development : Configuration("dev")
data object Staging : Configuration("staging")
data object Production : Configuration("prod")
data class Custom(val name: String) : Configuration(name)
companion object {
fun getDefault(env: String): Configuration = when (env) {
"dev" -> Development
"staging" -> Staging
"prod" -> Production
else -> Custom(env)
}
}
}
Nested and Companion Object Subclasses
Sealed classes support various subclass types, including nested classes, inner classes, companion objects, and regular objects. Each form serves different use cases. Objects work well for representing singleton states or values, while classes (data or regular) suit scenarios requiring multiple instances or value-based equality.
The Kotlin documentation notes that you can use enum classes within sealed classes to combine enumerated states with additional subclass flexibility. This hybrid approach allows enum constants to represent discrete states while sealed subclasses provide the full power of class hierarchies. This approach is particularly useful when building AI automation solutions where state management is critical for handling complex workflows.
Inheritance Rules and Restrictions
Subclass Placement Requirements
Direct subclasses of sealed classes must be declared in the same package. They may be top-level declarations or nested inside any number of other named classes, named interfaces, or named objects. This restriction ensures that the compiler can easily track all possible subtypes without searching across module boundaries.
Subclasses must have properly qualified names according to Kotlin naming rules. They cannot be local classes (defined inside functions) or anonymous objects (created with object syntax without a name). These restrictions maintain the type system's ability to reason about all possible subtypes at compile time.
package com.example.errors
// All subclasses must be in the same package
sealed class AppError {
// Nested class - allowed
data class ValidationError(val field: String) : AppError()
// Object subclass - allowed
data object UnknownError : AppError()
// Nested inside another class - allowed
sealed class NetworkErrors : AppError() {
class Timeout : NetworkErrors()
class ConnectionLost : NetworkErrors()
}
}
// This would NOT compile - different package
// package com.other
// class ExternalError : AppError() // Error!
Indirect Subclasses and Open Extensions
The restrictions on subclass placement apply only to direct subclasses. Indirect subclasses--classes that inherit from sealed class subclasses--follow normal inheritance rules. If a direct subclass is marked as open rather than sealed, it can be extended anywhere it is visible. This design allows for sophisticated hierarchies where the sealed boundary exists at one level while subsequent levels remain open for extension.
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Failure(val error: Throwable) : Result<Nothing>()
}
// Direct subclasses are sealed - cannot extend Result directly
// But Success is open for extension
class CachedSuccess<T>(val data: T, val timestamp: Long) : Result.Success<T>(data)
// This cached success can be used anywhere Result is expected
fun handleResult(result: Result<String>) {
when (result) {
is Result.Success -> println("Data: ${result.data}")
is Result.Failure -> println("Error: ${result.error}")
}
}
Multiplatform Project Considerations
Kotlin Multiplatform projects introduce additional inheritance restrictions for sealed classes. Direct subclasses must reside in the same source set. This rule applies to sealed classes without expected and actual modifiers, ensuring that platform-specific implementations don't break the sealed contract across platforms.
When using sealed classes with expected and actual declarations, both versions can have subclasses in their respective source sets. The hierarchical structure feature of Kotlin Multiplatform allows creating subclasses in any source set between the expect and actual declarations, providing flexibility while maintaining type safety.
For cross-platform mobile applications built with Kotlin Multiplatform, understanding these restrictions is crucial for designing effective sealed hierarchies that work across iOS, Android, and other platforms. These patterns are essential for SaaS application development where consistent type safety across all client implementations is required.
Using Sealed Classes with When Expressions
Exhaustive Pattern Matching
The true power of sealed classes emerges when combined with Kotlin's when expression. The when expression, used with a sealed class, allows the Kotlin compiler to perform exhaustiveness checking. This means the compiler verifies that all possible subtypes are handled in the when branches, eliminating the need for else clauses and preventing runtime errors from unhandled cases.
When you add a new subclass to a sealed class, the compiler will flag all when expressions that don't handle the new type. This feedback loop catches bugs early in development rather than at runtime. The exhaustive checking works because the compiler knows the complete set of possible subtypes at compile time.
sealed class PaymentMethod {
data class CreditCard(val number: String, val expiryDate: String) : PaymentMethod()
data class PayPal(val email: String) : PaymentMethod()
data object Cash : PaymentMethod()
data class BankTransfer(val accountNumber: String) : PaymentMethod()
}
fun processPayment(method: PaymentMethod, amount: Double) {
val message = when (method) {
is PaymentMethod.CreditCard -> "Processing credit card payment"
is PaymentMethod.PayPal -> "Processing PayPal payment"
is PaymentMethod.Cash -> "Processing cash payment"
is PaymentMethod.BankTransfer -> "Processing bank transfer"
// No else needed - compiler knows all cases are covered
}
println(message)
}
Type Extraction and Smart Casting
When using is checks in when branches, Kotlin's smart casting feature automatically casts the expression to the specific subtype. This eliminates the need for manual casting and makes the code both safer and more concise. The smart casting works because the compiler knows that if a when branch matches a specific subtype, the variable must be of that type within that branch.
The combination of sealed classes, when expressions, and smart casting creates a powerful trio for building type-safe code. Each branch operates on the specific subtype with full type information available, enabling you to access properties and call methods that exist only on that particular subclass.
Guard Conditions in When Expressions
Kotlin allows guard conditions in when expressions, enabling additional checks within a single branch. This feature proves useful when you need to handle subtypes differently based on their property values while maintaining exhaustiveness guarantees for type coverage.
sealed class OrderStatus {
data class Pending(val createdAt: Long) : OrderStatus()
data class Processing(val startedAt: Long) : OrderStatus()
data class Shipped(val trackingNumber: String, val shippedAt: Long) : OrderStatus()
data class Delivered(val receivedAt: Long) : OrderStatus()
data class Cancelled(val reason: String, val cancelledAt: Long) : OrderStatus()
}
fun describeOrder(status: OrderStatus): String {
return when (status) {
is OrderStatus.Pending -> "Order is pending since ${status.createdAt}"
is OrderStatus.Processing -> "Processing started at ${status.startedAt}"
is OrderStatus.Shipped -> {
if (status.shippedAt < System.currentTimeMillis() - 86400000) {
"Shipped over 24 hours ago: ${status.trackingNumber}"
} else {
"Recently shipped: ${status.trackingNumber}"
}
}
is OrderStatus.Delivered -> "Delivered at ${status.receivedAt}"
is OrderStatus.Cancelled -> "Cancelled: ${status.reason}"
}
}
This pattern is particularly valuable when building enterprise applications where complex business logic requires careful handling of different states and conditions. When combined with comprehensive web development practices, sealed classes help ensure that every possible state is accounted for and handled appropriately.
Sealed Classes vs Enum Classes
Fundamental Differences
While sealed classes and enum classes both represent restricted sets of values, they serve fundamentally different purposes. Enum classes enumerate singleton values of a single type, while sealed classes define a type hierarchy where each subclass can have its own state and behavior. Understanding these differences helps you choose the right tool for each situation.
Enum classes are ideal when you have a fixed set of related constants. Each enum entry is a singleton--you cannot create additional instances beyond the predefined ones. Enums excel at representing categories, states, or modes that don't require additional data. The enum class itself can have properties and methods, but each entry inherits these uniformly.
Sealed classes, by contrast, allow each subtype to be a distinct type with its own properties. You can have multiple instances of each subtype (unless using object subclasses), and each subtype can have completely different properties. This flexibility makes sealed classes suitable for representing complex domain objects that share a common base type.
When to Use Each
Use enum classes when you need a simple enumeration of related constants. Enums work well for representing days of the week, cardinal directions, fixed states like loading/success/error where no additional data is needed, or any scenario where the set of values is truly fixed and each value is a singleton.
Use sealed classes when you need to represent a type hierarchy with different subtypes that may have different properties. Sealed classes excel at representing different kinds of events, states with associated data, results that may carry different payloads, or domain objects that share a common interface but differ significantly in their structure.
// Enum for simple constants
enum class Direction {
NORTH, SOUTH, EAST, WEST
}
// Sealed class for complex states with different data
sealed class AuthState {
data object Unauthenticated : AuthState()
data class Authenticating(val attemptCount: Int) : AuthState()
data class Authenticated(val userId: String, val token: String) : AuthState()
data class Failed(val reason: String, val remainingAttempts: Int) : AuthState()
}
// The auth state carries different data depending on the state,
// something enums cannot express without workarounds
Combining Both Approaches
Kotlin allows enum classes to implement sealed interfaces, combining the singleton nature of enums with the hierarchy capabilities of sealed interfaces. This hybrid approach works when you want enumerated constants that also participate in a sealed hierarchy.
sealed interface TokenType
enum class BasicTokenType : TokenType {
ACCESS, REFRESH, ID
}
sealed class Token : TokenType {
data class Basic(val type: BasicTokenType, val value: String) : Token()
data class OAuth(val accessToken: String, val refreshToken: String?) : Token()
}
Understanding when to use sealed classes versus enums is fundamental to building robust API integrations where type-safe result handling is essential. These type system patterns are a cornerstone of clean code practices that help teams maintain large Kotlin codebases over time.
Practical Use Cases
UI State Management
Sealed classes shine in UI state management, where screens can be in various states like loading, displaying data, handling errors, or showing empty states. Each state may carry different data, and the sealed hierarchy ensures all states are explicitly handled.
// Comprehensive UI state for a user profile screen
sealed class ProfileUiState {
data object Loading : ProfileUiState()
data class Success(
val userId: String,
val name: String,
val email: String,
val avatarUrl: String?,
val bio: String?
) : ProfileUiState()
data class Error(
val message: String,
val errorCode: Int,
val canRetry: Boolean
) : ProfileUiState()
data object Empty : ProfileUiState()
}
// In a ViewModel or Presenter
fun renderProfile(state: ProfileUiState) {
when (state) {
is ProfileUiState.Loading -> showProgressBar()
is ProfileUiState.Success -> displayUserProfile(state)
is ProfileUiState.Error -> showErrorWithRetry(state)
is ProfileUiState.Empty -> showEmptyState()
}
}
This pattern provides several benefits. The sealed hierarchy documents all possible states explicitly. The when expression guarantees no states are missed. Each state type can carry exactly the data needed for that state. Adding new states is a controlled process that the compiler will flag in all relevant when expressions.
API Request and Response Handling
Sealed classes excel at modeling API interactions where responses can succeed with various data types, fail with different error types, or represent loading states. This approach creates a complete type-level representation of the API contract.
// Sealed interface for API requests
sealed interface ApiRequest<out R> {
data class GetUser(val userId: String) : ApiRequest<UserResponse>
data class UpdateProfile(val name: String, val email: String) : ApiRequest<Unit>
data class DeleteAccount(val reason: String) : ApiRequest<Unit>
}
// Sealed class for API responses
sealed class ApiResponse<out T> {
data class Success<T>(val data: T, val statusCode: Int) : ApiResponse<T>()
data class Error(val message: String, val statusCode: Int, val details: String?) : ApiResponse<Nothing>()
data object NetworkError : ApiResponse<Nothing>()
data object Timeout : ApiResponse<Nothing>()
}
// Handler function with exhaustiveness
fun <T> handleApiResponse(response: ApiResponse<T>): Result<T> {
return when (response) {
is ApiResponse.Success -> Result.success(response.data)
is ApiResponse.Error -> Result.failure(ApiException(response.message, response.statusCode))
is ApiResponse.NetworkError -> Result.failure(NetworkException())
is ApiResponse.Timeout -> Result.failure(TimeoutException())
}
}
Error Handling and Domain Exceptions
Representing domain errors as sealed classes provides type-safe error handling that distinguishes between different error conditions while maintaining exhaustiveness guarantees.
sealed class ValidationError {
data class RequiredField(val fieldName: String) : ValidationError()
data class InvalidFormat(val fieldName: String, val expectedFormat: String) : ValidationError()
data class OutOfRange(val fieldName: String, val min: Int, val max: Int) : ValidationError()
data class DuplicateEntry(val fieldName: String, val value: String) : ValidationError()
}
sealed class BusinessError {
data object InsufficientFunds : BusinessError()
data object AccountLocked : BusinessError()
data object PermissionDenied : BusinessError()
data class ResourceNotFound(val resourceType: String, val id: String) : BusinessError()
}
// Generic error type combining validation and business errors
sealed class AppError {
data class Validation(val errors: List<ValidationError>) : AppError()
data class Business(val error: BusinessError) : AppError()
data class System(val exception: Throwable) : AppError()
}
fun handleError(error: AppError) {
when (error) {
is AppError.Validation -> error.errors.forEach { println(it) }
is AppError.Business -> handleBusinessError(error.error)
is AppError.System -> logSystemError(error.exception)
}
}
Business Logic and Workflow States
Sealed classes naturally model state machines and workflow transitions, where each state represents a distinct phase with its own rules and possible transitions. This pattern is essential for SaaS application development where complex business workflows require precise state tracking and type safety.
sealed class OrderWorkflowState {
data object Draft : OrderWorkflowState()
data class PendingReview(val submittedAt: Long) : OrderWorkflowState()
data class Approved(val approvedBy: String, val approvedAt: Long) : OrderWorkflowState()
data class Processing(val startedAt: Long) : OrderWorkflowState()
data class Shipped(val trackingNumber: String, val carrier: String) : OrderWorkflowState()
data class Delivered(val receivedAt: Long) : OrderWorkflowState()
data class Cancelled(val reason: String, val cancelledAt: Long) : OrderWorkflowState()
data class ReturnRequested(val reason: String, val requestedAt: Long) : OrderWorkflowState()
}
class OrderWorkflow(private var state: OrderWorkflowState = OrderWorkflowState.Draft()) {
fun transition(action: OrderAction): Boolean {
val newState = when (val current = state) {
is OrderWorkflowState.Draft -> when (action) {
is OrderAction.Submit -> OrderWorkflowState.PendingReview(System.currentTimeMillis())
is OrderAction.Cancel -> OrderWorkflowState.Cancelled("Draft cancelled", System.currentTimeMillis())
}
is OrderWorkflowState.PendingReview -> when (action) {
is OrderAction.Approve -> OrderWorkflowState.Approved("Admin", System.currentTimeMillis())
is OrderAction.Reject -> OrderWorkflowState.Cancelled("Rejected", System.currentTimeMillis())
is OrderAction.Cancel -> OrderWorkflowState.Cancelled("Cancelled by user", System.currentTimeMillis())
}
// Additional states...
else -> null
}
return if (newState != null) {
state = newState
true
} else {
false
}
}
}
These practical patterns demonstrate why sealed classes are indispensable for building robust, maintainable applications with Kotlin. Whether you're working on mobile applications or enterprise-scale web solutions, sealed classes provide the type safety guarantees that prevent bugs before they happen.
Frequently Asked Questions
Can sealed classes have abstract members?
Yes, sealed classes can have abstract properties and methods that subclasses must implement. This allows you to define a common interface while maintaining the sealed hierarchy.
What happens if I add a new subclass to a sealed class?
The Kotlin compiler will flag all when expressions that don't handle the new type, ensuring you update all relevant code to handle the new case.
Can sealed classes implement interfaces?
Yes, sealed classes can implement any interface, and sealed interfaces can extend other interfaces. This enables sophisticated type hierarchies.
Are sealed classes thread-safe?
Sealed class definitions themselves are thread-safe, but thread safety of instances depends on their implementation. Object subclasses are singletons while data class instances follow normal concurrency rules.
How do sealed classes differ from Java sealed types?
Kotlin's sealed classes inspired Java's sealed classes but have some differences. Kotlin sealed classes are more flexible with subclass visibility rules and better integration with when expressions.
Can I serialize sealed classes?
Yes, most serialization libraries like Kotlinx Serialization, Jackson, and Gson support sealed classes. The polymorphic serialization requires proper configuration of the discriminator.
Conclusion
Sealed classes represent a powerful tool in the Kotlin type system, enabling developers to create type-safe hierarchies that combine compile-time exhaustiveness guarantees with the flexibility of class-based designs. By restricting inheritance to a known set of subclasses, sealed classes provide the compiler with enough information to verify that all possible cases are handled, eliminating entire categories of runtime errors.
The practical applications of sealed classes span across modern software development, from UI state management and API response handling to domain modeling and workflow state machines. Their ability to model complex domains while maintaining type safety makes them invaluable for building robust, maintainable applications.
As you incorporate sealed classes into your Kotlin projects, remember to leverage them where they provide clear benefits: when you need exhaustive pattern matching, when different subtypes carry different data, and when you want to model a closed set of related types. Combined with Kotlin's when expression and smart casting, sealed classes help build applications where the type system works proactively to prevent bugs rather than merely documenting code behavior.
Key Takeaways
- Use sealed classes when you need exhaustive pattern matching with different subtypes
- Prefer data classes for value-carrying subclasses
- Design hierarchies around single, coherent concepts
- Let the compiler help you maintain exhaustiveness
Further Learning
- Explore Kotlin's official documentation on sealed classes
- Practice with state management patterns in Android development
- Study domain-driven design principles for sealed class modeling
Sources
- Kotlin Documentation - Sealed Classes - Official Kotlin documentation provides comprehensive coverage of sealed classes, their syntax, inheritance rules, constructors, and use cases
- Reflectoring - Sealed Classes vs Enums in Kotlin - Detailed comparison article explaining when to use sealed classes versus enums, with practical examples