Type Checks & Smart Casts (is, as)

Reviewed & published by Brayan K

Type checks let you ask what kind of value you hold at runtime with the is operator, and smart casts then let the compiler treat that value as its real type automatically — no manual casting needed.

Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

You'll also learn the unsafe cast as, the safe cast as?, and how when combines with is.

What You'll Learn in This Lesson

1️⃣ Checking Types with is / !is

The is operator returns a Boolean telling you whether a value is of a given type at runtime; !is negates it. This is how you safely inspect a value whose static type is broad, like Any.

fun main() {
    val value: Any = "hello"

    // 'is' tests the runtime type, returning a Boolean:
    println(value is String)          // -> true
    println(value is Int)             // -> false

    // '!is' is the negation:
    println(value !is Int)            // -> true

    val number: Any = 42
    println(number is Int)            // -> true
}

2️⃣ Smart Casts

Here's the magic: once an is check succeeds, the compiler smart-casts the value inside that block, so you can use the narrowed type's members directly — no as required. It works because the compiler can prove the value didn't change.

fun main() {
    val obj: Any = "Kotlin"

    // After 'is' passes, the compiler SMART-CASTS obj to String —
    // so you can call String methods with no manual cast:
    if (obj is String) {
        println(obj.length)           // -> 6
        println(obj.uppercase())      // -> KOTLIN
    }

    val n: Any = 10
    if (n is Int) {
        // n is now treated as Int here:
        println(n + 5)                // -> 15
    }
}

3️⃣ Explicit Casts: as vs as?

When you must cast explicitly, choose carefully. The unsafe cast as throws a ClassCastException if the type is wrong. The safe cast as? returns null instead — pair it with the Elvis operator for a clean fallback.

fun main() {
    val any: Any = "world"

    // 'as' is an UNSAFE cast — it throws if the type is wrong.
    val text = any as String
    println(text.uppercase())         // -> WORLD

    // 'as?' is a SAFE cast — it yields null instead of throwing.
    val wrong = any as? Int           // not an Int -> null
    println(wrong)                    // -> null

    // Combine as? with Elvis for a clean fallback:
    val len = (any as? String)?.length ?: -1
    println(len)                      // -> 5
}

The most idiomatic pattern of all is when combined with is — each branch smart-casts automatically:

fun main() {
    // 'when' + 'is' inspects an unknown value and smart-casts in each branch:
    fun describe(x: Any): String = when (x) {
        is Int    -> "int doubled = ${x * 2}"
        is String -> "string of length ${x.length}"
        is Boolean -> "boolean is $x"
        else      -> "unknown"
    }

    println(describe(7))              // -> int doubled = 14
    println(describe("hey"))          // -> string of length 3
    println(describe(true))           // -> boolean is true
}

Your turn. Fill in the ___ blanks, then run and compare.

// 🎯 YOUR TURN — replace each ___ then run main()
fun main() {
    val item: Any = "code"

    // 1) Check whether item is a String
    println(item ___ String)          // 👉 is  -> true

    // 2) Smart-cast inside the if, then print its length
    if (item is String) {
        println(item.___)             // 👉 length -> 4
    }

    // ✅ Expected:
    // true
    // 4
}

Mini-Challenge: Type-Aware Printer

Write a when (x) { is ... } that labels a value by its type.

// 🎯 MINI-CHALLENGE: a type-aware printer.
// Save as TypePrinter.kt and run main().
//
// Write a function label(x: Any): String that uses
// when + is to return:
//   • "Number: <x>"  when x is an Int
//   • "Text: <x>"    when x is a String
//   • "Other"        otherwise
//
// Then print label(99) and label("hi").
//
// ✅ Expected output:
//    Number: 99
//    Text: hi

fun main() {
    // your code here
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Type Checks & Casts

GoalKotlin Syntax
Type checkx is String
Negated checkx !is Int
Smart castif (x is String) x.length
Unsafe castx as String
Safe castx as? String
Safe cast + default(x as? Int) ?: 0
Branch on typewhen (x) { is Int -> ... }

🎉 Lesson Complete!

Practice quiz

What does the is operator return?

  • The casted value
  • A new object
  • A Boolean telling whether the value is of that type
  • Always null

Answer: A Boolean telling whether the value is of that type. is is a runtime type check that evaluates to a Boolean.

What does !is mean?

  • A bitwise operation
  • An assignment
  • Force-cast the value
  • The negation of an is check

Answer: The negation of an is check. !is is true when the value is NOT of the given type.

After if (x is String) succeeds, what does the compiler do inside the block?

  • Smart-casts x to String automatically
  • Throws an exception
  • Forces you to write (x as String)
  • Sets x to null

Answer: Smart-casts x to String automatically. A smart cast lets you use String members directly with no manual cast.

What happens with val n = x as Int when x is actually a String?

  • It returns null
  • It throws a ClassCastException
  • It returns 0
  • It compiles to false

Answer: It throws a ClassCastException. as is an unsafe cast; a wrong type throws ClassCastException at runtime.

What does the safe cast as? do when the cast cannot succeed?

  • Throws an exception
  • Crashes the program
  • Returns null instead of throwing
  • Returns the original value

Answer: Returns null instead of throwing. as? yields null when the value is not of the target type.

Which expression is the safest explicit cast with a fallback?

  • x as Int
  • (x !is Int)
  • x is Int
  • (x as? Int) ?: 0

Answer: (x as? Int) ?: 0. Combine the safe cast as? with the Elvis operator for a default.

In a when (x) block, what does an is branch give you?

  • A smart cast inside that branch
  • A compile error
  • A null value
  • An infinite loop

Answer: A smart cast inside that branch. Each is branch smart-casts x to that type for use in the branch.

What is Any in Kotlin's type system?

  • A nullable-only type
  • The root of the non-null type hierarchy
  • A keyword for casting
  • A collection type

Answer: The root of the non-null type hierarchy. Every non-null type is a subtype of Any, like Object in Java.

Why prefer an is check over an explicit as cast?

  • as is faster
  • is only works on numbers
  • An is check is both a test and a safe smart cast, so it is safer and clearer
  • as is required for smart casts

Answer: An is check is both a test and a safe smart cast, so it is safer and clearer. An is check tests and smart-casts, avoiding the risk of as throwing.

Smart casts apply most reliably to which kind of variable?

  • A reassignable var
  • A global mutable property
  • A lateinit field
  • An immutable val (or a parameter)

Answer: An immutable val (or a parameter). The compiler can prove an immutable val did not change between check and use.

Continue this course

Frequently asked questions

What exactly is a smart cast?

A smart cast is the compiler automatically treating a value as a more specific type after you have proven its type with an is check (or a null check). Once if (x is String) succeeds, inside that block x is known to be a String, so you can call String methods directly without writing (x as String). It works because the compiler can see that x could not have changed between the check and the use — which is why it applies reliably to immutable local vals.

What is the difference between as and as?

Both perform a cast, but they fail differently. The unsafe cast as throws a ClassCastException at runtime if the value is not actually of the target type. The safe cast as? returns null instead of throwing when the cast cannot succeed. Prefer as? combined with the Elvis operator — for example (x as? String) ?: "" — so a wrong type produces a sensible fallback rather than a crash.

Why prefer is checks over casting with as?

An is check is both a test and, thanks to smart casts, an implicit safe cast — so you rarely need an explicit as at all. Writing if (x is String) { x.length } is safer and clearer than casting and hoping the type is right. Reach for as or as? only when you cannot restructure the code around an is check, such as when assigning to a typed variable in one expression.

What is Any, and how does it relate to type checks?

Any is the root of Kotlin's type hierarchy — every non-null type is a subtype of Any, much like Object in Java. A variable typed as Any can hold a value of any type, which is exactly when you need is checks and when expressions to discover and act on the real type at runtime. Its nullable form, Any?, can additionally hold null.