skip to content

Smart Cast After Null/Type Check

Once you have written if (x != null) or an is-check, the compiler treats x as the narrowed type for the rest of that branch, no cast needed. The interesting part is how flow-sensitive it is: it works across && and ||, inside when arms, and after an early return.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

What is a smart cast in Kotlin, and how does it work after an `if (x != null)` or `is` check?

level: juniorimportance: must knowfreq 80%

answer

  1. Check first, then use without a cast
  2. `if (x != null)` -> x is non-null inside
  3. `if (x is T)` -> x is T inside
  4. Compiler proves it, no `as` needed
  5. Works on stable `val`

basics

~10 s

After you check a variable for null or for a type, Kotlin automatically treats it as the checked type inside that block. You can use it without writing an explicit cast.

solid answer

~40 s

A smart cast is the compiler automatically narrowing a variable's type after a successful check, so no explicit `as` cast is needed. After `if (x != null)`, inside the `if` block `x` is treated as the non-null type (e.g. `String` instead of `String?`), so you can call `x.length` directly. After `if (x is String)`, `x` is treated as `String` inside the block. The compiler tracks this via flow analysis: it knows the check must have passed to reach that code. It works for `val` of stable types; the result is identical to writing `(x as String)` but the compiler proves it's safe, so there's no `ClassCastException` risk and no runtime cast cost beyond the original check.

code

kotlin · 10 lines
kotlin
fun render(value: Any?) {
    if (value is String) {
        // smart-cast to String
        println(value.uppercase())
    }
    if (value != null) {
        // smart-cast to Any (non-null)
        println(value.hashCode())
    }
}

go deeper

for a junior

Knows that after if (x != null) or is you can use the variable without a cast, and can write a basic example.

for a middle

Explains it's compiler flow analysis on stable vals and relates it to explicit as/as?.

for a senior

Distinguishes the static-type narrowing from runtime behavior, notes stability requirements, and the negative-branch / !is case.

for a principal

Frames smart casts within Kotlin's flow-sensitive type system and explains why stability (val, no custom getter, same module) is required for soundness.

## What a smart cast is A **smart cast** is the Kotlin compiler **automatically narrowing the static type** of a variable after it has been **proven** to hold a certain type or to be non-null. You do not write `as`; the compiler inserts the cast for you because flow analysis guarantees it is safe. Kotlin distinguishes **nullable** types (`String?`, can hold `null`) from **non-null** types (`String`). You cannot call members on a nullable type directly. A null check removes the nullability *within the proven region*. ## After a null check ```kotlin fun describe(x: String?) { if (x != null) { // here x is smart-cast to String (non-null) println(x.length) // no ?. needed } } ``` Inside the `if (x != null)` block, `x` has type `String`, so `x.length` compiles. Outside the block it is `String?` again. ## After a type (`is`) check ```kotlin fun length(any: Any): Int { if (any is String) { // any is smart-cast to String here return any.length } return -1 } ``` The `is` operator checks the runtime type; on success the variable is treated as that type. `!is` smart-casts in the *negative* branch / after an early return. ## Why it is safe The compiler only smart-casts when it can **prove** the value cannot change between the check and the use — primarily for `val` local variables and `val` properties of stable types. The generated bytecode still contains the check; the smart cast just lets the source code skip the redundant explicit `as`. ## Relation to explicit cast `x as String` is an **explicit** cast that throws `ClassCastException` if wrong. A smart cast is the compiler doing the same narrowing for you only when it has already verified the type, so it never fails at that point. Key operators/keywords: `is`, `!is`, `as`, `as?`, `!=`, `==`.

  • Does a smart cast still emit a runtime type check in the bytecode?
    Yes — the `is`/`!= null` check itself runs at runtime; the smart cast only removes the need for a redundant explicit `as` in the source.
  • What's the difference between `as` and a smart cast?
    `as` is an explicit cast that can throw `ClassCastException`; a smart cast is the compiler narrowing the type only after it has already proven the type, so it cannot fail there.

Like a bouncer checking ID at the door: once you're inside, everyone knows you're of age, no one re-checks.

saying these in an interview costs you the question

  • Says you still must write `as String` after the null check
  • Thinks smart cast changes the runtime value rather than the static type
  • Confuses smart cast with the `!!` not-null assertion operator
  • Believes smart cast can throw ClassCastException at the use site

context

open as a page

How does smart casting work with early returns and negated checks (e.g. `if (x == null) return`)?

level: middleimportance: must knowfreq 70%

basics

~10 s

If you return early when the value is null, the compiler knows that after that point the value cannot be null, so it smart-casts it to non-null for the rest of the function.

open as a page

How do `&&` and `||` enable smart casting within a single boolean expression?

level: middleimportance: should knowfreq 60%

basics

~20 s

With &&, a check on the left makes the variable narrowed on the right, because the right side only runs if the left was true. With ||, the right side runs only when the left was false, so the opposite narrowing applies.

open as a page

Compare a smart cast after `if (x != null)` with the `!!` not-null assertion and with `?.`/`let`. When is each appropriate?

level: seniorimportance: should knowfreq 50%

basics

~20 s

A smart cast lets the compiler prove the value is non-null after a check, with no risk. !! forces non-null and crashes if you're wrong. ?./let handle the null case gracefully. Prefer smart cast or safe calls; use !! rarely.

open as a page

How do smart casts behave across `when` branches, including `when (val v = ...)` subjects and exhaustiveness?

level: seniorimportance: should knowfreq 55%

basics

~10 s

In a when, each branch automatically narrows the value to whatever that branch tested for — a type or non-null check — so you can use it directly inside that branch.

open as a page