skip to content

Explain the nullability and smart-cast implications of takeIf, including pitfalls when the receiver is already nullable or when chaining several takeIf calls.

level: seniorimportance: should knowfreq 30%

answer

  1. Result is T? even on non-null receiver
  2. Nullable receiver -> use ?.takeIf so it is non-null
  3. (x as? T)?.takeIf { } combines cast + gate
  4. Smart-cast does NOT leak out of the predicate
  5. Stacked takeIf = layered nullability; collapse the predicate

basics

~10 s

takeIf always returns a nullable type, even on a non-null receiver. On a nullable receiver use ?.takeIf so the predicate sees a non-null it. Chaining many takeIf calls layers nullability and hurts readability.

solid answer

~50 s

`takeIf` returns `T?`, so it *introduces* nullability even when the receiver was non-null — the result usually needs `?.` or `?:` afterward. When the receiver is already nullable (`T?`), prefer `value?.takeIf { ... }`: the `?.` short-circuits on null so the predicate's `it` is the non-null `T`, and the overall result stays `T?`. Without the `?.`, you'd be calling `takeIf` on `T?` and the lambda would receive a nullable `it`. A useful pattern is `(x as? Foo)?.takeIf { it.valid }` to combine a safe cast with a gate. Smart-casts don't propagate *out* of a `takeIf` lambda — proving non-null inside the predicate doesn't make the outer receiver non-null, because the compiler can't assume the predicate's truth implies anything about later code. Stacking multiple `takeIf`/`takeUnless` calls compounds `T?` and obscures intent; a `when` or guard clauses read better.

code

kotlin · 7 lines
kotlin
fun parsePort(raw: String?): Int? =
    raw?.toIntOrNull()                 // Int?
       ?.takeIf { it in 1..65535 }     // ?. so `it` is non-null Int

// Bad: treating the result as non-null
val s: String = "x"
val len = s.takeIf { it.isNotEmpty() }.length  // does NOT compile: receiver is String?

go deeper

for a junior

Recognizes the result is nullable and that ?: or ?. usually follows.

for a middle

Uses ?.takeIf on nullable receivers and understands the result type stays T?.

for a senior

Explains that smart-casts don't escape the predicate, combines as? with takeIf, and avoids stacked gates by collapsing predicates.

for a principal

Defines team idioms for nullable gating, weighing expression fluency against readability, and reviews for the no-smart-cast-escape and over-chaining traps.

## takeIf introduces nullability Even though `takeIf` returns the *same* receiver when the predicate holds, its declared return type is `T?`: ```kotlin val s: String = "hi" val r = s.takeIf { it.length > 1 } // r: String? (nullable!) ``` So after `takeIf` you almost always need `?.`, `?:`, `!!` (rarely), or a null check. Forgetting this and treating `r` as non-null is the most common mistake. ## Nullable receivers: use ?.takeIf If the receiver is already `T?`, calling `takeIf` directly passes a nullable `it` into the predicate: ```kotlin val maybe: String? = readLine() // maybe.takeIf { it.isNotBlank() } // it is String? -> need ?. or null handling val ok = maybe?.takeIf { it.isNotBlank() } // ?. short-circuits; it is String (non-null) ``` The **safe-call `?.`** means: if `maybe` is null, the whole chain is null and the predicate never runs; otherwise `it` is the smart-cast non-null `String`. Result type is still `String?`. ## Combining with safe cast as? ```kotlin val valid = (any as? Account)?.takeIf { it.balance >= 0 } ``` `as?` yields `Account?`; `?.takeIf` gates only when the cast succeeded. Clean way to express "is the right type AND satisfies a rule". ## Smart-cast does NOT escape the lambda Proving something inside the predicate doesn't narrow the type outside it: ```kotlin val node: Node? = find() val ready = node?.takeIf { it.state == State.READY } // Outside, `node` is still Node?; the compiler did not narrow it. // Only `ready` carries the gated value (Node?). ``` The predicate returns a `Boolean`; the compiler cannot infer that a `true` result implies anything for subsequent statements, so no smart-cast leaks out. Use the *returned* value, not the original receiver. ## Chaining pitfalls Stacking gates layers nullability and hides intent: ```kotlin // Hard to read — every step can drop to null config.takeIf { it.enabled } ?.takeIf { it.port in 1..65535 } ?.takeUnless { it.host.isBlank() } ?: defaultConfig ``` This is equivalent to a single predicate or a guard block and reads worse. Prefer: ```kotlin config.takeIf { it.enabled && it.port in 1..65535 && it.host.isNotBlank() } ?: defaultConfig ``` or explicit `if`/`when` when the logic grows. ## Performance note Both are `inline`, so the lambdas don't allocate; the cost is purely conceptual (nullability/readability), not runtime.

  • Why use value?.takeIf { } rather than value.takeIf { } when value is nullable?
    The ?. short-circuits on null so the predicate never runs and its it parameter is the non-null type; calling takeIf directly would expose a nullable it.
  • Does proving non-null inside a takeIf predicate smart-cast the outer variable?
    No. The lambda only returns a Boolean; the compiler can't conclude anything about the outer receiver, so no smart-cast escapes. Use the returned (gated) value instead.

saying these in an interview costs you the question

  • Treating the takeIf result as non-null
  • Calling takeIf on a nullable receiver without ?. and getting a nullable it
  • Expecting a smart-cast to leak from the predicate to outer code
  • Deeply chaining takeIf instead of collapsing the predicate or using when
  • Reaching for !! to undo the nullability takeIf intentionally added

context