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.