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
- Checking types with is and !is
- How smart casts narrow a type after an is check
- The unsafe cast as and when it throws
- The safe cast as? that yields null instead
- Using when with is to branch on type
- The role of Any as the root type
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)
- ❌ val n = x as Int when x is a String throws ClassCastException. ✅ Use x as? Int and handle null.
- ❌ Calling (x as String).length defensively everywhere. ✅ Do one is check and rely on the smart cast.
- ❌ "Smart cast to String is impossible, because x is a mutable property". ✅ Copy to a local val first, then check it.
- ❌ Forgetting the else branch in a when used as an expression. ✅ Add an else to cover every case.
Pro Tips
- 💡 Prefer is over as: an is check plus smart cast is almost always cleaner and safer.
- 💡 as? + Elvis is the safest explicit cast: (x as? T) ?: default.
- 💡 when + is reads like a typed switch and smart-casts in every branch.
📋 Quick Reference — Type Checks & Casts
| Goal | Kotlin Syntax |
|---|---|
| Type check | x is String |
| Negated check | x !is Int |
| Smart cast | if (x is String) x.length |
| Unsafe cast | x as String |
| Safe cast | x as? String |
| Safe cast + default | (x as? Int) ?: 0 |
| Branch on type | when (x) { is Int -> ... } |
🎉 Lesson Complete!
- ✅ is / !is test a value's runtime type
- ✅ After an is check, the compiler smart-casts automatically
- ✅ as is unsafe (throws); as? is safe (yields null)
- ✅ (x as? T) ?: default is the safest explicit cast
- ✅ when + is branches on type and smart-casts each branch
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
- Previous: Objects & Companion Objects
- Next: Visibility Modifiers — public, private, protected and internal — and proper encapsulation
- Quick reference: Kotlin cheat sheet › Null Safety
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.