skip to content

How do smart casts behave across `when` branches, including `when (val v = ...)` subjects and exhaustiveness?

level: seniorimportance: should knowfreq 55%

answer

  1. Each `is`/`null` branch narrows the subject
  2. Subjectless `when` narrows per branch condition
  3. `when (val v = expr)` -> stable, evaluated once
  4. sealed -> exhaustive, no else, each branch smart-cast
  5. Narrowing feeds the when's result type

basics

~10 s

In a when, each branch automatically narrows the value to whatever that branch tested for — a type or non-null check — so you can use it directly inside that branch.

solid answer

~40 s

`when` participates fully in smart casting. In a `when (x)` with `is`/`null` conditions, each branch smart-casts `x` to the matched type within that branch's body. In a subjectless `when { cond -> ... }`, each branch narrows according to its boolean condition exactly like `if`. You can bind a stable subject with `when (val v = expr)` so the (possibly side-effecting) expression is evaluated once and `v` is a `val`, making it smart-cast-eligible. When a sealed hierarchy is covered, the `when` is **exhaustive** and needs no `else`; each `is Subtype` branch smart-casts to that subtype. Narrowing also benefits the type of the whole `when` expression. The key requirement, as always, is subject stability — `when (val v = ...)` is the idiom that guarantees it.

code

kotlin · 8 lines
kotlin
sealed interface Event
data class Click(val x: Int, val y: Int) : Event
data class Key(val code: Int) : Event

fun describe(e: Event): String = when (e) {
    is Click -> "click ${e.x},${e.y}" // e: Click
    is Key   -> "key ${e.code}"        // e: Key
} // exhaustive over the sealed interface, no else

go deeper

for a junior

Knows that each is/null branch lets you use the value as that type without a cast.

for a middle

Explains both subject and subjectless forms narrow, and writes when (val v = ...) for a call subject.

for a senior

Connects sealed exhaustiveness to no-else smart-cast branches and explains the value of when (val v = ...) for stability and single evaluation.

for a principal

Reasons about exhaustiveness as a maintenance safety net (new subtype -> compile error) and the interaction of branch narrowing with the inferred result type.

## Two forms of `when` ### Subject `when` ```kotlin fun area(shape: Any?): Double = when (shape) { null -> 0.0 is Circle -> Math.PI * shape.r * shape.r // shape: Circle is Rect -> shape.w * shape.h // shape: Rect else -> -1.0 } ``` Each `is`/`null` branch smart-casts the subject inside that branch. The `null` branch and a non-null branch together fully classify a nullable subject. ### Subjectless `when` ```kotlin fun size(x: Any?): Int = when { x == null -> 0 x is String -> x.length // smart-cast String x is Collection<*> -> x.size // smart-cast Collection else -> -1 } ``` Here each branch condition narrows just like an `if` condition. ## `when (val v = ...)` — stabilizing the subject If the subject is a function call or an unstable expression, capture it in a local `val`: ```kotlin when (val response = fetch()) { is Success -> show(response.data) // response: Success, evaluated once is Error -> log(response.cause) // response: Error } ``` This evaluates `fetch()` exactly once and makes `response` a stable `val`, so smart casts are guaranteed in each branch. Without the binding you'd have to call `fetch()` repeatedly or it might not be smart-cast-eligible. ## Exhaustiveness with sealed types ```kotlin sealed interface Result data class Ok(val value: Int) : Result data class Fail(val msg: String) : Result fun handle(r: Result) = when (r) { is Ok -> r.value // smart-cast Ok is Fail -> r.msg.length // smart-cast Fail } // no else needed: the sealed hierarchy is exhaustive ``` Because `sealed` declares a closed set of subtypes, the compiler knows the branches are exhaustive; `when` used as an expression must be exhaustive, and each `is` branch smart-casts. Adding a new subtype later turns this into a compile error until handled — a key benefit. ## Smart cast affects the result type The narrowed type in each branch lets you call subtype members and influences the inferred type of the whole `when` expression. Keywords/operators: `when`, `is`, `else`, `sealed`, `val` subject binding, exhaustive.

  • Why prefer `when (val v = fetch())` over `when (fetch())`?
    It evaluates `fetch()` exactly once and binds a stable `val`, guaranteeing smart casts in each branch and avoiding repeated side-effecting calls.
  • What happens to an exhaustive sealed `when` when you add a new subtype?
    The `when` is no longer exhaustive, so used as an expression it becomes a compile error, forcing you to handle the new case — a deliberate safety feature.

A mail sorter: once a letter falls into the 'parcels' bin, every worker at that bin knows it's a parcel and handles it as one.

saying these in an interview costs you the question

  • Adds a redundant `else` to a sealed exhaustive `when`, defeating the new-subtype compile check
  • Calls a side-effecting subject expression in multiple branches instead of `when (val v = ...)`
  • Doesn't know subjectless `when` smart-casts per condition
  • Thinks `when` branches need an explicit `as` cast

context