skip to content

Inside `fun <T> T?.ifNull(default: T): T`, how do you correctly handle `this` being null, and what role do smart casts play?

level: middleimportance: should knowfreq 55%

answer

  1. `this` is `T?` inside the body
  2. Elvis `?:` for one-line fallback
  3. `if (this != null)` → smart cast to `T`
  4. Unbounded `T` only gives Any? members
  5. Avoid `!!` — defeats the purpose

basics

~10 s

Inside, this may be null, so test it. After if (this != null) the compiler narrows the type to non-null, letting you use it safely. Or just use Elvis: return this ?: default.

solid answer

~40 s

With a nullable generic receiver `T?`, `this` has type `T?` inside the body. You handle null with the Elvis operator (`return this ?: default`), a safe call (`this?.member`), or an explicit `this == null` / `this != null` check. After such a check, Kotlin applies a **smart cast**: in the branch where `this != null`, `this` is treated as `T` (non-null) so you can access members without `?.`. Smart casts work for `val`/stable receivers; the receiver `this` is stable here. With generics, accessing members of `T` is limited unless `T` is bounded (e.g. `<T : Comparable<T>>`), but null handling itself is independent of the bound. Avoid `!!` — it defeats the point and can throw. Prefer Elvis for a single fallback expression; use explicit checks when you need a multi-statement non-null branch.

code

kotlin · 12 lines
kotlin
fun <T> T?.ifNull(default: T): T = this ?: default

fun <T : Any> T?.describe(): String {
    if (this == null) return "<none>"
    return "value=$this hash=${hashCode()}" // smart-cast to T
}

fun main() {
    val x: Int? = null
    println(x.ifNull(42))       // 42
    println(x.describe())       // <none>
}

go deeper

for a junior

Can use Elvis ?: to return a default when this is null.

for a middle

Explains smart casting after a null check and the Any? bound limit on unbounded T.

for a senior

Chooses between Elvis, safe call, and explicit branches deliberately and bounds T when richer members are needed.

for a principal

Reasons about how nullable generic receivers shape a reusable utility API and stability requirements for smart casts in concurrent/var contexts.

## The setup ```kotlin fun <T> T?.ifNull(default: T): T = this ?: default ``` Here `T` is a **generic type parameter** and the receiver type is `T?` — a nullable receiver whose underlying type is generic. Inside the body, `this` has type `T?`. ## Three ways to handle null in the body **1. Elvis operator `?:`** — concise, expression-form: ```kotlin fun <T> T?.ifNull(default: T): T = this ?: default ``` Reads: if `this` is non-null use it, else use `default`. **2. Safe call `?.`** — call a member only when non-null, yielding null otherwise: ```kotlin fun <T> T?.orToString(): String? = this?.toString() ``` **3. Explicit check + smart cast** — when you need multiple statements: ```kotlin fun <T : Any> T?.describe(): String { if (this == null) return "<none>" // smart cast: here `this` is T (non-null) return "value=$this hash=${hashCode()}" } ``` ## Smart casts A **smart cast** is the compiler automatically treating a value as a more specific (here, non-null) type after a check, without an explicit cast. After `if (this != null)`, inside that branch `this` is `T`, so member access needs no `?.`. Requirements: the checked entity must be **stable** (a `val`, or the receiver `this`); a mutable `var` captured by a closure may block the smart cast. ## Generics caveat `T` is unbounded, so its upper bound is `Any?`. You can call `toString()`, `hashCode()`, `equals()` (members of `Any?`), but nothing more specific unless you **bound** `T`: ```kotlin fun <T : Comparable<T>> T?.maxWith(other: T): T = if (this == null) other else maxOf(this, other) ``` ## Why not `!!`? `this!!` throws `NullPointerException` if null — that re-introduces the very crash a nullable receiver is meant to avoid. Use it only when null is genuinely impossible and you can document why. ## Summary - `this` is `T?` inside the body. - Elvis `?:` for a single fallback; `?.` for null-tolerant member access; explicit check for multi-statement branches. - Smart casts narrow `this` to `T` after `!= null`. - Bound `T` if you need members beyond `Any?`.

  • Why can you only call `toString()`, `equals()`, `hashCode()` on an unbounded `T`?
    An unbounded type parameter `T` has the implicit upper bound `Any?`, exposing only `Any`'s members. To call more, add a bound like `<T : Comparable<T>>` or `<T : SomeInterface>`.
  • When can a smart cast fail to apply to `this`?
    `this` (the receiver) is stable, so it usually works. Smart casts fail on mutable `var`s, custom getters, or values modifiable between check and use — but those don't apply to the receiver itself.

saying these in an interview costs you the question

  • Using `this!!` instead of null-safe handling
  • Assuming `this` is non-null without a check
  • Thinking you can call arbitrary methods on unbounded `T`
  • Confusing the receiver's nullability with the type parameter's bound
  • Believing Elvis only works on String/collections

context