Explain how the compiler decides a `when` over a sealed type is exhaustive, including the role of subtype visibility/module boundaries and nullability of the subject.
answer
- Exhaustive needs a compiler-known closed set
- Sealed subtypes: same module -> closed
- Consumers in other modules can still match exhaustively
- Nullable subject -> null is an extra required case
- is-branches smart-cast inside the branch
basics
~20 sThe compiler can only check a when if it knows every subtype. Sealed types declare all their subtypes up front, so it can. If the value can be null, you must also handle the null case, or the when is not complete.
solid answer
~50 sExhaustiveness checking requires the compiler to know the **complete, closed set** of possible cases. A `sealed class`/`sealed interface` enumerates all direct subtypes, which must reside in the **same module** and (in modern Kotlin) the same package or compilation; the compiler reads that closed set and verifies each is matched (typically via `is` branches), allowing omission of `else`. Because the set is closed within the module, **consumers in other modules can still match exhaustively** as long as the sealed declaration and all permitted subtypes are visible to them at compile time — sealed subtypes are part of the type's public API surface. Nullability matters: if the subject's type is **nullable** (`T?`), the `null` value is an additional case, so an exhaustive `when` must include a `null ->` branch (or handle it before the `when`), otherwise it is non-exhaustive. The same closed-set logic applies to `enum class` (all entries) and `Boolean` (`true`/`false`). For open or unbounded types the set is infinite, so only `else` makes the `when` exhaustive.
code
kotlin · 9 linessealed interface Result
data class Ok(val v: Int) : Result
data object Fail : Result
fun render(r: Result?): String = when (r) {
null -> "empty" // needed: r is nullable
is Ok -> r.v.toString()
Fail -> "failed"
} // exhaustive, no elsego deeper
Understands the compiler needs to know all cases and that null counts as a case for nullable subjects.
Explains the closed-set requirement for sealed/enum/Boolean and adds the null branch correctly.
Reasons about module-scoped sealed closure, cross-module exhaustiveness, and the refactoring safety net it provides.
Weighs API design: where to place sealed types, whether consumers should add defensive else, and the cross-module blast radius of adding a subtype.
## The core rule: exhaustiveness needs a closed set The compiler can prove a `when` covers everything only when it knows the **finite, closed set** of possible subject values: - **`enum class`** — its entries. - **`Boolean`** — `true` / `false`. - **`sealed class` / `sealed interface`** — all permitted direct subtypes. For `Int`, `String`, or an `open class`, the set is effectively unbounded, so only an `else` branch makes the `when` exhaustive. ## How sealed closure works across modules A sealed type's subtypes must be declared in the **same module** as the sealed type (and, in current Kotlin, the same package/compilation unit). This is what makes the set *closed*: no other module can sneak in a new subtype. Key consequence: **other modules can still write exhaustive `when`s** over your sealed type. Exhaustiveness is not blocked at module boundaries — the closed subtype set is part of the type's compile-time API, so a downstream consumer that can see the sealed type and its subtypes can match all of them without `else`. ```kotlin // module A sealed interface Result data class Ok(val value: Int) : Result data class Fail(val error: String) : Result // module B (depends on A) fun show(r: Result): String = when (r) { is Ok -> "ok ${'$'}{r.value}" is Fail -> "fail ${'$'}{r.error}" // exhaustive without else: A's subtype set is closed } ``` ## Nullability adds a case If the subject is **nullable**, `null` is an extra value the `when` must handle: ```kotlin fun show(r: Result?): String = when (r) { null -> "none" // required because r is Result? is Ok -> "ok" is Fail -> "fail" } ``` Drop the `null ->` branch and the `when` becomes **non-exhaustive** (compile error for an expression). Equivalent options: handle null before the `when` (e.g. `r ?: return ...`) so the subject inside is non-null, or use `else`. ## `is` branches and smart-casting Matching subtypes with `is Ok` / `is Fail` also **smart-casts** the subject inside each branch, so subtype members are accessible without explicit casts. This pairs naturally with sealed exhaustiveness. ## Practical implications - Adding a subtype in module A breaks every exhaustive `when` in modules B, C... that omitted `else` — a cross-module refactoring safety net. - A defensive `else` in a library *consumer* trades that safety for resilience to upstream additions; choose deliberately. - `enum` + `when` and `sealed` + `when` share the same closed-set machinery; nullability is an orthogonal extra case for both. ## Summary Exhaustiveness = compiler-known closed set + every case (including `null` when the subject is nullable) covered. Sealed closure is module-scoped but visible to consumers, so exhaustiveness travels across module boundaries.
- If a sealed type lives in module A, can module B write an exhaustive `when` without `else`?Yes. A's subtype set is closed and visible to B at compile time, so B can match all subtypes exhaustively; adding a subtype in A will then break B's `when`.
- What changes about exhaustiveness if the subject type is `T?` instead of `T`?`null` becomes an additional case. The `when` must include a `null ->` branch (or otherwise eliminate null beforehand) to remain exhaustive.
saying these in an interview costs you the question
- Saying exhaustiveness is impossible across module boundaries
- Forgetting that a nullable subject needs a null branch
- Thinking sealed subtypes can be added from any module
- Confusing closed-set checking with runtime type inspection
- Believing only the declaring module can match exhaustively