Kotlin Type Checking and Smart Casts

Kotlin provides is to check types at runtime and as to cast between types. After an is check, Kotlin automatically narrows the variable's type — a feature called smart cast — so you don't need to cast it manually.

is — Type Check

fun describe(value: Any) {
    if (value is String) {
        println("String of length ${value.length}")   // value is auto-cast to String here
    } else if (value is Int) {
        println("Integer: ${value * 2}")              // auto-cast to Int
    } else if (value is List<*>) {
        println("List with ${value.size} items")      // auto-cast to List
    } else {
        println("Unknown type: ${value::class.simpleName}")
    }
}

fun main() {
    describe("Hello")
    describe(42)
    describe(listOf(1, 2, 3))
    describe(3.14)
}

Output:

String of length 5
Integer: 84
List with 3 items
Unknown type: Double

Smart Cast: How It Works


Without smart cast (Java style — verbose):
  if (value is String) {
      val str = value as String   // manual cast required
      println(str.length)
  }

With smart cast (Kotlin):
  if (value is String) {
      println(value.length)       // already a String — no cast needed!
  }

Kotlin's compiler tracks the is-check and automatically treats
the variable as the checked type inside the branch.

Smart Cast in when

sealed class Shape
data class Circle(val radius: Double) : Shape()
data class Rectangle(val width: Double, val height: Double) : Shape()
data class Triangle(val base: Double, val height: Double) : Shape()

fun area(shape: Shape): Double = when (shape) {
    is Circle    -> Math.PI * shape.radius * shape.radius   // smart cast to Circle
    is Rectangle -> shape.width * shape.height              // smart cast to Rectangle
    is Triangle  -> 0.5 * shape.base * shape.height         // smart cast to Triangle
}

fun main() {
    val shapes = listOf(Circle(5.0), Rectangle(4.0, 6.0), Triangle(3.0, 8.0))
    shapes.forEach { println("Area: ${"%.2f".format(area(it))}") }
}

Output:

Area: 78.54
Area: 24.00
Area: 12.00

as — Unsafe Cast

val obj: Any = "Hello"
val str = obj as String       // succeeds — obj is a String
println(str.length)           // 5

val num: Any = 42
// val bad = num as String    // throws ClassCastException at runtime!

as? — Safe Cast

val value: Any = 42

val asString = value as? String   // null if cast fails — no exception
val asInt    = value as? Int      // 42

println(asString)   // null
println(asInt)      // 42

// Safe cast with Elvis fallback
val result = (value as? String) ?: "Not a string"
println(result)     // Not a string

Unsafe vs Safe Cast Comparison


as (unsafe):
  val s = obj as String
  If obj is not a String → ClassCastException → app crashes

as? (safe):
  val s = obj as? String
  If obj is not a String → returns null → no crash
  Use with ?: to provide a fallback

Smart Cast Limitations

class Container(var value: Any)

val c = Container("hello")

// Smart cast does NOT work on mutable var properties
// because another thread could change c.value between the check and use:
if (c.value is String) {
    // println(c.value.length)   // ERROR: smart cast impossible on var property
    val v = c.value as String   // must cast manually
    println(v.length)
}

// Smart cast DOES work on local val variables:
val local: Any = "world"
if (local is String) {
    println(local.length)   // OK — local val cannot change
}

Practical Example: Event Dispatcher

sealed class AppEvent
data class LoginEvent(val username: String, val ip: String) : AppEvent()
data class PurchaseEvent(val itemId: String, val amount: Double) : AppEvent()
data class ErrorEvent(val code: Int, val message: String) : AppEvent()

fun handleEvent(event: AppEvent) {
    when (event) {
        is LoginEvent   -> println("User '${event.username}' logged in from ${event.ip}")
        is PurchaseEvent -> println("Purchase: item ${event.itemId} for ₹${event.amount}")
        is ErrorEvent   -> println("ERROR ${event.code}: ${event.message}")
    }
}

fun main() {
    val events = listOf(
        LoginEvent("alice", "192.168.1.10"),
        PurchaseEvent("PROD-042", 1499.0),
        ErrorEvent(500, "Database connection lost"),
        LoginEvent("bob", "10.0.0.5")
    )
    events.forEach { handleEvent(it) }
}

Output:

User 'alice' logged in from 192.168.1.10
Purchase: item PROD-042 for ₹1499.0
ERROR 500: Database connection lost
User 'bob' logged in from 10.0.0.5

Leave a Comment

Your email address will not be published. Required fields are marked *