How do `is`/`!is` branches work in `when`, and how do smart casts apply inside those branches?
answer
- `is`/`!is` = instanceof / not
- Smart cast = automatic, no `as` needed
- Needs a stable val; var/open property blocks it
- `when (val x = ...)` enables smart cast
- `is A, is B` only smart-casts to common supertype
basics
~20 sis Type matches when the subject is that type; !is is the opposite. Inside an is branch Kotlin smart-casts the value to that type automatically, so you can call its members without an explicit cast.
solid answer
~50 sIn a subject `when`, `is String -> ...` matches when the subject is an instance of `String`; `!is String` is the negation. Within the matched branch Kotlin applies a **smart cast**: the subject is statically known to be that type, so you access type-specific members with no `as` cast. Smart casting requires the value to be stable — a `val`, or a `val`-captured subject via `when (val x = ...)`; a mutable `var` (especially one that could change between the check and use, or an open property) blocks it. For sealed hierarchies, `is` branches over each subtype make the `when` exhaustive without `else`, and each branch is smart-cast to its subtype. You can combine `is` with comma grouping, but smart casts only apply when the type is unambiguous; a branch like `is A, is B ->` smart-casts only to the common supertype.
code
kotlin · 7 linesfun render(node: Any?): String = when (node) {
is String -> "text:${'$'}{node.length}" // smart-cast String
is Int -> "int:${'$'}{node + 0}" // smart-cast Int
is List<*> -> "list:${'$'}{node.size}" // smart-cast List<*>
null -> "null"
else -> "unknown"
}go deeper
Knows is/!is test types and that smart cast removes the need for as in a basic is branch.
Explains smart-cast stability — local val works, mutable var property may not — and uses when (val x = ...).
Articulates module/open/custom-getter limits, comma-group common-supertype rule, and sealed exhaustiveness via is.
Designs sealed hierarchies and API boundaries so smart casts work cleanly and exhaustiveness is enforced at compile time.
## `is` / `!is` type checks `is Type` is Kotlin's instance check (Java's `instanceof`); `!is Type` is the negation: ```kotlin when (obj) { is String -> obj.length // smart-cast to String is List<*> -> obj.size // smart-cast to List<*> !is Number -> handleNonNumber(obj) else -> 0 } ``` ## Smart casts A **smart cast** is the compiler automatically treating a value as a more specific type after a successful `is` check, so you don't write `as`: ```kotlin when (x) { is String -> x.uppercase() // x: String here, no cast needed is Int -> x + 1 // x: Int here else -> "?" } ``` Inside the `is String` branch, `x` has type `String` for the whole branch body. ### Stability requirements Smart casts only work when the compiler can prove the value **won't change** between the check and the use: - **Local `val`** — always smart-castable. - **`when (val v = expr)`** — captures the subject as a `val`, enabling smart casts even when the source expression is a property/function call. - **Blocked** for: local `var` modified in a closure, **mutable `var` properties**, `open`/custom-getter properties, and properties from another module — because their value could change or be overridden. The fix is to copy into a local `val` first. ```kotlin // Smart cast on a property may be blocked: class Box(var item: Any) fun show(b: Box) = when { b.item is String -> (b.item as String).length // var property -> may need explicit cast else -> 0 } // Better: fun show2(b: Box) = when (val it = b.item) { is String -> it.length // captured val -> smart cast OK else -> 0 } ``` ## With sealed classes (intersection with exhaustiveness) When the subject is a `sealed` type, `is`-branches over every subtype make the `when` exhaustive without `else`, and each branch smart-casts to that subtype: ```kotlin sealed interface Shape data class Circle(val r: Double) : Shape data class Square(val side: Double) : Shape fun area(s: Shape) = when (s) { is Circle -> Math.PI * s.r * s.r // s: Circle is Square -> s.side * s.side // s: Square } ``` ## `is` with comma grouping `is A, is B -> ...` matches either type, but inside the body the value is only smart-cast to the **common supertype** of A and B (often `Any`), since the compiler can't know which one matched. If you need members of A specifically, split the branch. ## `!is` `!is Type` matches when the value is NOT that type; there's no smart cast to `Type` in that branch (the opposite — the value is known *not* to be `Type`).
- Why might a smart cast fail on a class property?If it's a mutable `var`, has a custom getter, is `open`, or lives in another module, its value could change/override between check and use, so the compiler won't smart-cast. Copy it into a local `val`.
- What type is smart-cast in `is A, is B -> body`?Only the common supertype of A and B, because the compiler can't tell which alternative matched.
An is check is like a bouncer confirming your VIP status; once confirmed, the whole branch treats you as VIP without re-checking your ID.
saying these in an interview costs you the question
- Adding an explicit `as` cast where a smart cast already applies
- Expecting smart cast on a mutable `var` property and not knowing why it fails
- Thinking `is A, is B` smart-casts to A or B specifically
- Believing `!is` smart-casts the value to the tested type