How do I use smart casting and flow control to eliminate redundant null checks in Kotlin?

In Kotlin, you can eliminate redundant null checks by letting the compiler smart cast a nullable value after you prove it is not null.

Basic smart cast

fun printLength(text: String?) {
    if (text != null) {
        println(text.length)
    }
}

Inside the if block, Kotlin knows text cannot be null, so it treats it as a non-null String.

You do not need this:

fun printLength(text: String?) {
    if (text != null) {
        if (text != null) {
            println(text.length)
        }
    }
}

The second check is redundant.

Use early returns for cleaner flow

A common Kotlin style is to return early when the value is null:

fun printLength(text: String?) {
    if (text == null) return

    println(text.length)
}

After the return, Kotlin knows that text must be non-null for the rest of the function.

This is useful when you want to avoid nesting:

fun processUserName(name: String?) {
    if (name == null) return

    println(name.uppercase())
    println(name.length)
}

Use Elvis with return

You can also combine the Elvis operator ?: with return:

fun processUserName(name: String?) {
    val nonNullName = name ?: return

    println(nonNullName.uppercase())
    println(nonNullName.length)
}

Here, if name is null, the function returns immediately. Otherwise, nonNullName is a non-null String.

Use Elvis with default values

If you want to continue with a fallback value instead of returning:

fun printLength(text: String?) {
    val value = text ?: ""

    println(value.length)
}

value is always a non-null String.

Use let for nullable scoped work

Use ?.let when you only want to run code if the value is non-null:

fun printLength(text: String?) {
    text?.let { nonNullText ->
        println(nonNullText.length)
    }
}

Inside the let block, nonNullText is non-null.

Smart casts with type checks

Smart casts also work with is checks:

fun printIfString(value: Any?) {
    if (value is String) {
        println(value.length)
    }
}

Inside the block, value is treated as String.

You can also invert the check:

fun printIfString(value: Any?) {
    if (value !is String) return

    println(value.length)
}

After the early return, Kotlin knows value is a String.

Combine conditions safely

Kotlin understands flow control in boolean expressions:

fun printLength(text: String?) {
    if (text != null && text.length > 3) {
        println(text.uppercase())
    }
}

Because text != null is checked first, text.length is safe.

This does not work if the order is reversed:

fun printLength(text: String?) {
    if (text.length > 3 && text != null) {
        println(text.uppercase())
    }
}

That fails because text.length is accessed before the null check.

Prefer immutable values

Smart casts work best with val values:

val name: String? = getName()

if (name != null) {
    println(name.length)
}

They may not work with mutable properties because the value could change between the check and the use:

var name: String? = getName()

if (name != null) {
    println(name.length)
}

Local var variables can sometimes be smart cast if the compiler can prove they are not modified, but mutable properties are more limited.

For properties, copy the value into a local val:

class User(var name: String?)

fun printUserName(user: User) {
    val name = user.name

    if (name != null) {
        println(name.length)
    }
}

Avoid !!

Instead of writing:

fun printLength(text: String?) {
    if (text != null) {
        println(text!!.length)
    }
}

write:

fun printLength(text: String?) {
    if (text != null) {
        println(text.length)
    }
}

The !! is unnecessary because smart casting already made text non-null.

Practical pattern

A concise, idiomatic pattern is:

fun handle(input: String?) {
    val text = input ?: return

    println(text.trim())
    println(text.length)
}

Use:

  • if (x != null) when you want a guarded block.
  • if (x == null) return when you want to avoid nesting.
  • val y = x ?: return when you want a non-null local variable.
  • x?.let { ... } when the work should happen only if x is non-null.
  • ?: defaultValue when you want to replace null with a fallback.