A few weeks ago we looked at one of my pain points of working with Kotlin Data Classes: nested copy and we improved on a basic Kotlin solution using a Lens. Today we look at another type of nesting: Sealed Classes nesting.
When we wanted to create some sort of restricted hierarchy in Java we were using enum. They are kinda OK for a few things, but they lack the power of stateful subclasses. Update 2026: While Kotlin 2.0 has arrived with massive compiler improvements, the fundamental “no copy for sealed classes” reality remains. Here is how to handle it like a pro.
The Kotlin way: Choice Types
Sealed Classes are “Choice types” or “Sum types”. They let you define a small set of possible types for an object. A classic example is PhoneNumber:
sealed class PhoneNumber {
data class Mobile(val number: String) : PhoneNumber()
data class Office(val number: String) : PhoneNumber()
data object Home : PhoneNumber() // Modern Kotlin uses 'data object'
}
Since Kotlin 1.9, we should use data object for cases without state (like Home above) to get better toString() and equals() implementations automatically.
The Problem: No .copy() at the top level
If you have a UserProfile containing a PhoneNumber, you can’t just call user.phone.copy() if phone is declared as the sealed base class. The compiler doesn’t know which specific subclass it is at runtime.
val updatedUser = user.copy(phone = user.phone.copy(number = "123")) // ❌ ERROR: Unresolved reference: copy
The Solution: Arrow Optics (Prisms)
Instead of writing messy when blocks to check every subclass, use a Prism from the Arrow Optics library. It allows you to focus on a specific branch of your sealed hierarchy and modify it in a single line.
@optics
sealed class ContactInformation {
companion object {}
data class Email(val value: String) : ContactInformation()
@optics data class Mobile(val number: String) : ContactInformation() { companion object }
}
// Update Joe's mobile number in one line:
val updatedJoe = UserProfile.contact.mobile.number.set(joe, "987654321")
FAQ: Kotlin Sealed Classes & Copy
Why do sealed classes not have a copy method in Kotlin?
The copy() method is generated only for data class subclasses. The base sealed class doesn’t have it because it doesn’t define the properties shared by all subclasses in a way that allows safe copying without type checking.
How to update a property inside a nested sealed class?
The standard way is using a when expression. The “Senior Dev” way is using Prisms from Arrow Optics to avoid nesting hell.
Is ‘data object’ preferred over ‘object’ in sealed classes?
Yes, since Kotlin 1.9, data object is preferred for subclasses that don’t hold state, as it provides a consistent toString() and equals() implementation.
Conclusions
Immutability doesn’t have to be painful. By leveraging the Kotlin ecosystem (JetBrains, Arrow, and the community), we can write code that is both robust and elegant. Stop fighting the compiler and start using Prisms. 💪🏻
Happy learning 😉
