Why is val ms = if (cond) listOf(1) else mutableListOf<Int>() a subtle pitfall, and what general principle does it illustrate about if expressions and types?
answer
- Branches join to least common supertype
- List + MutableList -> List (mutable API lost)
- Widening can make the var less capable
- Fix: explicit target type or same branch types
- Always read the inferred type when branches differ
basics
~20 sThe two branches have different list types, so the variable gets the broader common type (List), losing the mutable API. Watch the inferred type: an if expression widens to whatever both branches share, which may be less than you expect.
solid answer
~40 sBoth branches contribute to the result type via the **least common supertype**. `listOf(1)` is `List<Int>` (read-only); `mutableListOf<Int>()` is `MutableList<Int>`. Their common supertype is `List<Int>`, so `ms` is inferred as `List<Int>` and you **lose** the `MutableList` API (`add`, `remove`) even though one branch was mutable. The general principle: an `if` expression's type is the join of all branches, so mixing a narrower and a wider type silently widens the variable — the inferred type can be less capable than any single branch you intended. Fixes: declare the target type explicitly (`val ms: MutableList<Int> = if (...) mutableListOf(1) else mutableListOf()`), make both branches the same type, or restructure. The same trap appears with nullability (`else null` → nullable) and unrelated types collapsing to `Any`. Always read the inferred type when branches differ.
code
kotlin · 6 linesval a = if (cond) listOf(1) else mutableListOf<Int>()
// a: List<Int> — a.add(2) won't compile
val b: MutableList<Int> =
if (cond) mutableListOf(1) else mutableListOf()
b.add(2) // OK — type explicitly pinned to MutableListgo deeper
Recognizes that the two branches have different list types and that something is off with mutability.
Explains read-only List vs MutableList and that the inferred type is the common List.
States the join/least-common-supertype principle and gives explicit-type or same-type fixes.
Generalizes to a code-review heuristic: scrutinize inferred types whenever if/when branches differ in type or nullability.
## The pitfall ```kotlin val ms = if (cond) listOf(1) else mutableListOf<Int>() ``` What type is `ms`? People often expect something mutable, but: - `listOf(1)` → `List<Int>` (Kotlin's **read-only** list interface; no `add`/`remove`). - `mutableListOf<Int>()` → `MutableList<Int>`. The `if` expression type is the **least common supertype** of the branches. `MutableList<Int>` is a subtype of `List<Int>`, so the join is `List<Int>`. Therefore `ms: List<Int>`, and calling `ms.add(2)` **does not compile** — even though one branch produced a `MutableList`. ```kotlin val ms = if (cond) listOf(1) else mutableListOf<Int>() // ms: List<Int> // ms.add(2) // ERROR: unresolved reference add ``` ## The general principle An `if` (and `when`) expression's static type is the **join of every branch type**. Mixing a narrower branch with a wider one silently widens the variable to the wider/common type. The result can be **less capable** than the branch you cared about. Related manifestations: - `if (c) 1 else null` → `Int?` (nullability injected). - `if (c) 1 else "x"` → `Any` (precision lost when types are unrelated). ## Fixes 1. **Declare the target type** to force coercion: ```kotlin val ms: MutableList<Int> = if (cond) mutableListOf(1) else mutableListOf() ``` 2. **Make both branches the same type** (both `MutableList`, or both `List`). 3. **Restructure** so the mutable list is created once, then conditionally populated: ```kotlin val ms = mutableListOf<Int>() if (cond) ms.add(1) ``` ## Why Kotlin does this Kotlin separates the **read-only** `List`/`Map`/`Set` interfaces from their `Mutable*` counterparts. The variance and subtyping mean the common supertype of read-only and mutable is the read-only one — a deliberate, safe default that nonetheless surprises if you don't read the inferred type. ## Keywords/APIs involved - `List` vs `MutableList`, `listOf`, `mutableListOf`, `emptyList`. - Least common supertype / type join during `if`/`when` inference. - Explicit type ascription to steer inference.
- How do you keep the MutableList API here?Declare val ms: MutableList<Int> = ... so both branches coerce to MutableList, or make both branches return mutableListOf.
- Is this specific to collections?No. Any branches with different types join to a common supertype: nullability via else null, or Any when types are unrelated. The collection case is just a vivid example.
Two tools go into one slot, but the holder only fits the common shape — you get the lesser tool's capabilities.
saying these in an interview costs you the question
- Assuming the result keeps the mutable type
- Not knowing List and MutableList are separate interfaces
- Thinking the inferred type is always the last branch's type
- Blaming the compiler instead of the missing type ascription
- Ignoring inferred types when branches differ