Skip to content
AgileLogics
4 min read

Type Checks and Casts

Prerequisites: Inheritance, Null safety

Sometimes you have a value of a general type — Any, or a superclass — and need to work with it as a more specific type.

is and smart casts

is checks a type. After a successful check, Kotlin smart-casts the value, so you can use the specific type's members directly:

fun describe(value: Any): String {
    if (value is String) {
        return "Text of length ${value.length}"     // value is String here
    }
    if (value !is Int) {
        return "Something else"
    }
    return "Number doubled: ${value * 2}"          // value is Int here
}

fun main() {
    println(describe("hello"))
    println(describe(21))
    println(describe(2.5))
}
Text of length 5
Number doubled: 42
Something else

when with is branches is the most common form, as you saw in the when lesson.

as and as?

  • as casts explicitly, throwing ClassCastException if the value is the wrong type.
  • as? is the safe cast: it returns null instead of throwing.
fun main() {
    val items: List<Any> = listOf("ada", 42, "linus", 3.5)

    val names = items.mapNotNull { it as? String }
    println(names)
    println(items.filterIsInstance<Int>())
}
[ada, linus]
[42]

filterIsInstance<T>() keeps only elements of a given type — and the result is already typed as a List<Int>.

When smart casts don't work

Smart casts only apply when the compiler can be sure the value can't change between the check and the use. For a var property of a class, another piece of code could change it in between, so copy it into a local val first:

class Box(var content: Any?)

fun main() {
    val box = Box("hello")
    val content = box.content
    if (content is String) {
        println(content.uppercase())
    }
}
HELLO

Common mistakes

  • Reaching for casts to decide behavior by type. Often a method on an interface, or a sealed type with an exhaustive when, is a cleaner design.