skip to content

How does Kotlin infer the type of an expression like `if`/`when` branches or `listOf(1, "a")`, and what subtle pitfalls (e.g. widening to `Any`, platform types, generic inference) should you watch for?

level: seniorimportance: should knowfreq 45%

answer

  1. Branches/collections -> least upper bound (common supertype)
  2. Mismatched branch silently widens to Any
  3. Java results are platform types (String!), assumed non-null
  4. mutableListOf() needs T from args or annotation
  5. Annotate public APIs and non-obvious cases

basics

~10 s

When values have different types, Kotlin picks the nearest common parent type. Mixing an Int and a String gives Any. Watch out: the inferred type can be wider than you want, hiding bugs.

solid answer

~50 s

For multi-branch expressions (`if`/`when`) or heterogeneous collections, Kotlin infers the **least upper bound** — the most specific common supertype of all branches. `if (c) 1 else 2` is `Int`; `if (c) 1 else "a"` is `Any` (technically `Comparable<*> & Serializable`-ish common type, surfaced as `Any`); `listOf(1, "a")` is `List<Any>`. The pitfall is **accidental widening**: an inferred `Any`/`Any?` silently swallows what you intended to be a narrower type, so a misplaced branch type compiles instead of erroring. Two more traps: (1) **platform types** from Java (`String!`) where inference can't know nullability, so `val s = javaCall()` may give a non-null-looking `String` that's actually nullable; annotate to be safe. (2) **generic inference** — `mutableListOf()` with no element can't infer `T`, requiring `mutableListOf<Int>()` or an annotation. Best practice: annotate where the inferred type is non-obvious or wider than intended, and at public API boundaries.

code

kotlin · 10 lines
kotlin
val r = when (s) {        // inferred Any — likely a bug
    OK -> 200
    else -> "error"
}
val r2: Int = when (s) { OK -> 200; else -> 500 } // annotation catches mistakes

val name = javaApi.getName()   // platform String — may be null at runtime
val safe: String? = javaApi.getName()

val xs = mutableListOf<Int>()  // explicit T needed (no elements to infer from)

go deeper

for a junior

Recognizes that mixing types in a list gives a broader type and may be surprising.

for a middle

Explains the common-supertype rule for if/when and basic generic inference needs.

for a senior

Identifies accidental-widening, platform-type, and target-typing pitfalls and how to guard with annotations.

for a principal

Sets team policy: annotate public APIs, treat Java nullability explicitly, and reason about LUB stability across refactors.

## Least upper bound (common supertype) When an expression has several possible result types, Kotlin computes the **least upper bound (LUB)** — the most specific type that is a supertype of all of them. ```kotlin val a = if (cond) 1 else 2 // Int (both Int) val b = if (cond) 1 else 2L // Long (Int & Long -> Long? no) -> actually Any-ish numeric LUB val c = if (cond) 1 else "x" // Any (no common numeric/text supertype but Any) val d = when (k) { 1 -> Dog(); else -> Cat() } // nearest common supertype, e.g. Animal ``` For collections it's the same idea over elements: ```kotlin val xs = listOf(1, 2, 3) // List<Int> val ys = listOf(1, "a") // List<Any> val zs = listOf(Dog(), Cat()) // List<Animal> if both extend Animal ``` ## Pitfall 1 — accidental widening to Any/Any? If one branch is the wrong type, inference quietly widens instead of erroring: ```kotlin val result = when (status) { OK -> 200 else -> "error" // oops: now result is Any, not Int } ``` The fix is to **annotate the expected type** so the wrong branch fails to compile: ```kotlin val result: Int = when (status) { OK -> 200; else -> 500 } ``` Nullability widens similarly: `if (c) x else null` becomes `T?`. ## Pitfall 2 — platform types from Java Java methods have no Kotlin nullability info, so their results are **platform types**, written `String!`. Inference adopts the platform type and treats it as **non-null by default**, deferring the null check: ```kotlin val s = javaApi.getName() // inferred as String (platform) — could be null at runtime! ``` Mitigation: annotate explicitly (`val s: String? = javaApi.getName()`) or rely on Java `@Nullable`/`@NotNull` annotations the compiler honors. ## Pitfall 3 — generic / call-site inference The compiler infers type arguments from arguments and the expected type. With no information it can't: ```kotlin // val xs = mutableListOf() // ERROR: cannot infer T val xs = mutableListOf<Int>() // OK val ys: MutableList<Int> = mutableListOf() // OK: T from expected type val z = emptyList<String>() // explicit arg needed ``` Kotlin can also infer from the **expected type** of the surrounding context (target typing), which is why `mutableListOf()` works when the variable is annotated. ## Pitfall 4 — over-relying on inference in public APIs An inferred return type couples your published type to the current implementation. Changing the body can change the inferred type and break callers. Prefer explicit types on public/protected members (and enforce via `explicitApi()`). ## Practical guidance - Annotate when the inferred type is **non-obvious, wider than intended, or part of a public API**. - Be suspicious of `Any`/`Any?` showing up unexpectedly — usually a branch-type mismatch. - Treat Java-returned values' nullability explicitly. - Provide explicit generic arguments when there's nothing to infer from.

  • What is the inferred type of `if (c) 1 else null`?
    `Int?` — the null branch makes the least upper bound nullable.
  • Why is `val s = javaCall()` risky?
    It's a platform type with unknown nullability, treated as non-null; a runtime null can cause an NPE. Annotate `String?` if it can be null.
  • How does `val xs: List<Int> = listOf()` infer T?
    From the expected (target) type of the variable, so `T` becomes `Int` even with no element arguments.

The LUB is like finding the nearest shared ancestor on a family tree: the closer the relatives, the more specific the inferred type; distant relatives collapse to the common root, Any.

saying these in an interview costs you the question

  • Not recognizing accidental widening to Any
  • Trusting platform-type nullability blindly
  • Thinking generic T is always inferable even with no arguments
  • Claiming `if/else` always yields the first branch's type
  • Never annotating public API return types

context