Sealed Classes
Mutate polymorphic hierarchies with cast() for individual elements
and filterMutatorsOf<T>() for typed collection access.
Defining a sealed hierarchy
Annotate the sealed interface (or sealed class) with @Mutable. The subtypes don't need separate
annotations — KSP generates mutators for the entire hierarchy:
@Mutable
sealed interface PaymentMethod {
data class CreditCard(
val number: String,
val expiry: String,
) : PaymentMethod
data class BankTransfer(
val iban: String,
val bic: String,
) : PaymentMethod
} Mutating with cast()
When you have a Mutator<PaymentMethod>, you don't know the concrete type at compile time. Use
cast() to narrow the type and mutate:
val methods: List<PaymentMethod> = listOf(
PaymentMethod.CreditCard("4111-1111-1111-1111", "12/25"),
PaymentMethod.BankTransfer("DE89370400440532013000", "COBADEFFXXX"),
)
val updated = methods.mutate {
forEach { mutator ->
when (val value = mutator()) {
is PaymentMethod.CreditCard ->
mutator.cast(value) { expiry = "12/28" }
is PaymentMethod.BankTransfer ->
mutator.cast(value) { bic = "NEWBICXXX" }
}
}
} The cast(value) call narrows the mutator's type parameter. Inside the lambda, you get typed property
access — expiry, bic, etc. — with full IDE completion and compile-time safety.
Filtering with filterMutatorsOf<T>()
For ListMutator and SetMutator of sealed types, KSP generates filterMutatorsOf<T>().
This returns only the mutators whose underlying value matches the specified subtype:
val methods: List<PaymentMethod> = listOf(
PaymentMethod.CreditCard("4111-1111-1111-1111", "12/25"),
PaymentMethod.BankTransfer("DE89...", "COBADEFFXXX"),
PaymentMethod.CreditCard("5500-0000-0000-0004", "06/26"),
)
val mutator = methods.mutator()
// Get only the credit card mutators — fully typed as Mutator<CreditCard>
val cards = mutator.filterMutatorsOf<PaymentMethod.CreditCard>()
cards.forEach { cardMutator ->
cardMutator.expiry = "12/30" // Typed access — no cast needed
} Indexed iteration with cast()
You can use forEachIndexed with sealed types just as you'd expect:
val items: List<SealedInterface> = listOf(
SealedInterface.One("hello"),
SealedInterface.Two(42),
)
val updated = items.mutate {
forEachIndexed { index, mutator ->
when (val v = mutator()) {
is SealedInterface.One -> mutator.cast(v) { value += "-$index" }
is SealedInterface.Two -> mutator.cast(v) { value += index }
}
}
}
// Result: [One("hello-0"), Two(43)] Generated code for sealed types
For a @Mutable sealed interface, KSP generates the same .mutator(),
.mutate { }, and collection extensions as for data classes. It additionally generates filterMutatorsOf
for ListMutator and SetMutator:
// Generated for List<PaymentMethod>:
inline fun <reified X : PaymentMethod>
ListMutator<PaymentMethod>.filterMutatorsOf(): List<Mutator<X>>
// Generated for Set<PaymentMethod>:
inline fun <reified X : PaymentMethod>
SetMutator<PaymentMethod>.filterMutatorsOf(): List<Mutator<X>> Each subtype also gets its own property accessors, so Mutator<CreditCard>.number and Mutator<BankTransfer>.iban
are fully typed.