Mastering Kotlin Smart Casts with Contracts: Beyond the Basics
Bridge the gap between custom helper functions and the Kotlin compiler to write cleaner, safer, and more idiomatic code.
Kotlin’s smart casting is a superpower. It allows the compiler to automatically infer types after a check, simplifying your code. But what happens when you abstract that logic into helper functions? Suddenly, the compiler loses its magic touch.
Enter Kotlin Contracts: a way to bridge the gap between your custom logic and the compiler’s static analysis.
The Smart Cast Dilemma
Consider a standard sealed hierarchy for a messaging app:
sealed interface Account
class Moderator(val badgeId: String) : Account
class StandardUser(val username: String) : AccountWhen you perform an explicit check, Kotlin is happy:
fun handleAccount(account: Account) {
if (account is Moderator) {
println(account.badgeId) // ✅ Smart-cast works!
}
}But the moment you move that check into a reusable function, the magic breaks:
fun isMod(account: Account): Boolean = account is Moderator
fun handleAccount(account: Account) {
if (isMod(account)) {
// ❌ Error: account is still just 'Account'
// println(account.badgeId)
}
}The compiler sees that isMod returns a Boolean, but it doesn't know why. It can't look inside the function body during the call-site analysis to realize that true implies Moderator.
The Solution: Kotlin Contracts
Contracts allow you to tell the compiler: “If this function returns true, you can trust that the argument is actually a specific type."
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@OptIn(ExperimentalContracts::class)
fun Account.isModerator(): Boolean {
contract {
// The Promise: If return is true, 'this' is definitely a Moderator
returns(true) implies (this@isModerator is Moderator)
}
return this is Moderator
}
fun process(account: Account) {
if (account.isModerator()) {
println("Accessing Badge: ${account.badgeId}") // ✅ Smart-cast restored!
}
}⚠️ The “Golden Rule” of Contracts: Don’t Lie
It is vital to understand that contracts are compiler-trust, not runtime-enforcement. The compiler does not verify that your contract is actually true; it simply takes your word for it.
Dangerous Code Example:
@OptIn(ExperimentalContracts::class)
fun Account.isModerator(): Boolean {
contract { returns(true) implies (this is Moderator) }
return true // ❌ WE ARE LYING!
}Result: At runtime, the compiler-generated cast will be applied blindly. If the object is actually a StandardUser, this will result in a ClassCastException. You have effectively bypassed Kotlin’s type safety.
Enhanced Nullability: The ensureNotNull Pattern
Contracts are also the secret sauce behind Kotlin’s standard library functions like requireNotNull. You can build your own validation helpers that clean up your null-handling:
@OptIn(ExperimentalContracts::class)
fun validateToken(token: String?) {
contract {
// If this returns (completes without exception), the token is not null
returns() implies (token != null)
}
if (token == null) throw IllegalArgumentException("Missing Token")
}
fun apiCall(token: String?) {
validateToken(token)
// No '?' needed below because the contract guaranteed non-nullability!
println("Token length: ${token.length}")
}Frequently Asked Questions (FAQs)
Does adding a contract slow down my app?
Not at all. Contracts are purely a compile-time tool. They are used by the compiler to verify your code and are erased during compilation. There is zero runtime overhead.
Where must the contract be placed?
It must be the very first statement in your function body. Even putting a println() before the contract block will cause a compilation error.
Why do I still need an else branch with sealed interfaces?
Even if you use a contract, if expressions in Kotlin are not exhaustive. The compiler requires an else branch for total safety to handle the false case, whereas a when expression on a sealed type directly would understand that no other types exist.
Can I use contracts in any function?
As of Kotlin 1.9 and 2.0, contracts remain experimental. They work best in top-level or extension functions. They are not supported in functions that can be overridden (like open class methods), as the compiler can’t guarantee a subclass will honor the same contract.
Let’s Discuss!
- Have you ever had to use an
as?cast just because a helper function broke your smart-cast? - Do you think the
@OptInrequirement makes contracts too "scary" for production teams? - Would you prefer “Lying Contracts” to be a compiler error, or do you like the flexibility they offer?
If you find my content useful, feel free to support my work here. Every contribution is greatly appreciated! 🙏

Comments
Post a Comment