Give cases where Kotlin refuses to smart-cast after an `is`/null check, and how you work around each.
answer
- stable value required
- var props, custom getter, open, other module = unstable
- snapshot into local val
- ?.let captures a stable receiver
- delegated (by) props not smart-cast
basics
~20 sKotlin won't smart-cast when the value could change between check and use: mutable var properties, properties with custom getters, open properties, and values from other modules. Copy the value into a local val (or use ?.let) and check that instead.
solid answer
~50 sSmart casts require a **stable** value. The compiler refuses to narrow when it can't guarantee the runtime type still holds at the point of use: - **`var` properties** (member or top-level): could be reassigned by another thread between check and use. - **Properties with a custom getter or `open` properties**: each access may return a different value, so checking once proves nothing. - **Delegated properties (`by`)**: value comes from the delegate's getter. - **Properties declared in another module**: the compiler can't see the backing field / mutability guarantees. - **`var` locals mutated by a closure** that could run between check and use. Workarounds: assign to a **local `val`** and check that (`val s = obj.name; if (s is String) ...`), use **`?.let { }`** which captures the receiver, or use the `!!` / explicit narrowing only when truly safe. Local `val`s and `val` properties with backing fields are always smart-castable.
code
kotlin · 8 linesclass Node { var next: Any? = null }
fun handle(n: Node) {
// n.next is a var property -> not smart-castable
val next = n.next // snapshot
if (next is String) {
println(next.uppercase()) // OK now
}
}go deeper
Recognizes the 'smart cast impossible' error and knows copying into a local val fixes it.
Lists the unstable categories (var, custom getter, open, other module, delegated) and the snapshot/let workarounds.
Explains the thread-safety and getter-reentrancy reasoning behind stability and chooses idiomatic fixes per case.
Designs APIs/properties to maximize smart-castability and weighs immutability vs. defensive copying across module boundaries.
## Why smart casts can fail A smart cast is only sound if the value the compiler checked is the *same* value you later use, with the *same* runtime type. Kotlin calls such values **stable**. When stability can't be proven, the compiler emits `Smart cast to 'T' is impossible, because '...' is a ...` and you must cast explicitly or restructure. ## The unstable cases ### 1. Mutable `var` properties ```kotlin class Box { var item: Any? = null } fun use(b: Box) { if (b.item is String) { // ERROR: b.item is a var property; another thread could reassign it // println(b.item.length) } } ``` ### 2. Custom getters / `open` properties ```kotlin val now: Any get() = System.currentTimeMillis() // returns a fresh value each call // if (now is Long) { now.toString() } // not stable — getter re-runs ``` An `open val` is also unstable because a subclass could override it with a custom getter. ### 3. Properties from another module Even a `val` property declared in a different compilation module isn't smart-cast by default, because the compiler treats it conservatively. ### 4. Delegated properties ```kotlin val lazyVal: Any by lazy { compute() } // value is produced through the delegate's getValue — not smart-castable ``` ## Workarounds ### Capture into a local `val` ```kotlin fun use(b: Box) { val item = b.item // stable local val if (item is String) println(item.length) // OK } ``` ### Use `?.let` ```kotlin (b.item as? String)?.let { println(it.length) } // or, staying within is-checks, capture first then let b.item?.let { value -> if (value is String) println(value.length) } ``` `let` captures the receiver into the lambda parameter, which is a stable `val`. ## What is always stable - local `val`s - `val` properties with a backing field in the **same module** that are not `open` and have no custom getter - the implicit receiver inside `let`/`run`/`apply` lambdas ## Key mental model If the value can be re-read and return something different (var, getter, delegate, cross-module), Kotlin won't smart-cast. Snapshot it into a `val` and the problem disappears.
- Why does `?.let { }` succeed where a direct `if (prop is T)` fails?`let` evaluates the receiver once and binds it to a stable lambda parameter (`it`), so subsequent uses reference that fixed value rather than re-reading the unstable property.
- Is a `val` property always smart-castable?No — not if it has a custom getter, is `open`, is delegated, or lives in another module. Plain `val`s with a backing field in the same module are.
saying these in an interview costs you the question
- Claiming any `val` is always smart-castable
- Saying smart casts fail randomly with no rule
- Not knowing the 'snapshot into local val' fix
- Believing custom getters are safe to smart-cast