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) returnwhen you want to avoid nesting.val y = x ?: returnwhen you want a non-null local variable.x?.let { ... }when the work should happen only ifxis non-null.?: defaultValuewhen you want to replacenullwith a fallback.
