In a `when` over a sealed type, how does smart-casting work inside each `is` branch, and what can prevent it?
answer
- is SubType -> auto-casts the subject in that arm
- Stability required: local val ok, member var not
- Custom getter / delegated / captured var blocks it
- Fix with when (val x = expr)
- Nullable sealed needs a null branch too
basics
~20 sInside an is Subtype -> branch the compiler already knows the value is that subtype, so it auto-casts it and you can use the subtype's properties directly. A mutable var that could change can block this.
solid answer
~40 sWhen a branch tests `is SubType`, the Kotlin compiler **smart-casts** the subject to `SubType` for the rest of that arm, so you access subtype-specific members without an explicit `as` cast. This relies on the compiler proving the value can't change between the check and the use. Smart-cast is blocked when the subject is a mutable `var` accessible from outside (e.g. a top-level/member `var`, or a `var` captured by a closure), a custom `val` property with a getter, or a delegated property — because another thread or getter call could return a different value. The fix is to bind the subject to a local `val` (often via `when (val x = expr)`), which is stable and freely smart-cast. Branches can also match by value or condition, but only `is`/null checks produce a type smart-cast.
code
kotlin · 11 linesclass Holder(var node: Node) // member var
fun bad(h: Holder) = when (h.node) {
is Text -> h.node.content // ERROR: smart cast impossible (var member)
else -> "?"
}
fun good(h: Holder) = when (val n = h.node) { // capture once into val
is Text -> n.content // OK: n is a stable local val
else -> "?"
}go deeper
Knows each is arm lets you use the subtype's members without casting.
Explains the stability requirement and that a member var or custom getter blocks smart-cast, with the when (val x = ...) fix.
Connects blocked smart-cast to thread-safety/re-evaluation soundness and knows delegated/open/captured cases.
Reasons about why the compiler demands stability for soundness and how API/property design (favor vals) keeps call sites smart-castable.
## Smart-cast basics A **smart-cast** is the compiler automatically treating a value as a more specific type after it has *proven* the type via a check like `is SubType` or `!= null`. No `as` keyword is needed. ```kotlin sealed interface Json data class JStr(val s: String) : Json data class JNum(val n: Double) : Json object JNull : Json fun render(j: Json): String = when (j) { is JStr -> j.s.uppercase() // j smart-cast to JStr -> .s available is JNum -> j.n.toString() // j smart-cast to JNum -> .n available JNull -> "null" // object singleton, equality match } ``` ## What the compiler must prove: stability Smart-cast is sound only if the value **cannot change** between the check and the use. The compiler allows it for **stable** subjects: - local `val`s, - the `when (val x = ...)` binding, - `val` parameters, - private/internal `val`s with no custom getter in the same module. ## What blocks smart-cast - A **mutable `var`** (local var is fine if no closure mutates it after; member/top-level `var` is not, since another thread could change it). - A **`val` with a custom getter** — calling it twice could yield different values. - **Delegated properties** (`by`), open `val`s overridable in subclasses, and vars **captured by a lambda**. For these the compiler errors: "Smart cast to 'X' is impossible, because '...' is a mutable property...". ## The standard fix: bind to a local val ```kotlin fun describe(node: Node) = when (val t = node.payload) { // local val = stable is Text -> t.content is Img -> t.url } ``` The `when (val t = ...)` subject form captures the value once into an immutable local, enabling smart-cast and avoiding re-evaluating the expression per branch. ## Match forms and smart-cast - `is SubType ->` smart-casts to `SubType`. - A bare `object` branch (e.g. `JNull ->`) matches by `==`; the type is already that object's type. - Guard/value conditions don't change the type. ## Nullability interplay If the sealed type is nullable (`Json?`), you must also handle `null` (e.g. a `null ->` branch) for exhaustiveness; after a non-null check the value is smart-cast to the non-null type.
- Why does a `val` with a custom getter block smart-cast?Each access invokes the getter, which can return a different value, so the type proven on the first call isn't guaranteed on the next — the compiler can't assume stability.
- How does `when (val x = expr)` help beyond enabling smart-cast?It evaluates `expr` exactly once and binds it to an immutable local, avoiding repeated evaluation and side effects across branches.
Once airport security checks your boarding pass for gate B, everyone downstream treats you as a gate-B passenger — but only if your pass can't be swapped mid-walk.
saying these in an interview costs you the question
- Thinks smart-cast works on any property regardless of mutability
- Adds explicit `as` casts everywhere, unaware smart-cast exists
- Doesn't know member `var` blocks smart-cast
- Confuses smart-cast with unsafe cast `as`
- Believes custom getters are smart-castable