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: DoubleSmart 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.00as — 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 stringUnsafe 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