Smart Casts Workbook

Ten short exercises on Kotlin smart casts — is and !is, null-check narrowing, smart casts in when, combined conditions, early returns, and where they don't apply.

Practice problems for Kotlin Smart Casts. Each takes a minute or two. A few auto-check: implement the function in the editor and press Run — hidden tests go green when you’re right and red (with a hint) when you’re not, all on JetBrains’ Kotlin server. The rest — the ones that print or make a point about the compiler — stay attempt-then-reveal: press Run to try them, then click Show answer. Nothing here is a trick question, just direct practice of the syntax from the lesson.

the basic cast

1. Check, then use

Given x: Any, print its length when it’s a String — without writing a cast.

Try it — edit, then press Run fun main() { val x: Any = "hello" if (x is String) { println(x) // TODO: print x.length instead (x is smart-cast to String here) } }
Show answer Hide answer
if (x is String) {
    println(x.length)   // x is smart-cast to String here
}

2. Negate and return early

Given x: Any, return early when it is not a String; afterward, use it as a String.

Try it — edit, then press Run fun main() { val x: Any = "hello" // TODO: return early when x is NOT a String println(x) // TODO: after the guard, print x.length (x is String here) }
Show answer Hide answer
if (x !is String) return
println(x.length)       // x is String from here on

3. Null check narrows

Given name: String?, print its length only when it isn’t null — no safe call, no !!.

Try it — edit, then press Run fun main() { val name: String? = "Ada" if (name != null) { println(name) // TODO: print name.length (name is smart-cast to String) } }
Show answer Hide answer
if (name != null) {
    println(name.length)   // name is smart-cast to String
}

in when and conditions

4. Smart cast inside when

Implement incIfInt so it returns x + 1 when x is an Int, else 0 — using the smart cast.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun incIfInt() { Assert.assertEquals("an Int is incremented", 42, incIfInt(41)) Assert.assertEquals("anything else is 0", 0, incIfInt("hi")) } } //sampleStart fun incIfInt(x: Any): Int = 0 // TODO: x + 1 when x is Int //sampleEnd
Show answer Hide answer
fun incIfInt(x: Any) = when (x) {
    is Int -> x + 1
    else -> 0
}

5. Combined condition

Implement isNonEmptyString so it returns true only when x is a non-empty String.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun isNonEmptyString() { Assert.assertTrue("a non-empty String is true", isNonEmptyString("hi")) Assert.assertFalse("an empty String is false", isNonEmptyString("")) Assert.assertFalse("a non-String is false", isNonEmptyString(42)) } } //sampleStart fun isNonEmptyString(x: Any): Boolean = x is String // TODO: also require the String to be non-empty //sampleEnd
Show answer Hide answer
fun isNonEmptyString(x: Any) = x is String && x.isNotEmpty()

After the is check on the left, x is smart-cast for the right-hand side.

6. Elvis with an early exit

Given value: Any, bind a String named s or return early if value isn’t one. Use a safe cast.

Try it — edit, then press Run fun main() { val value: Any = "hello" val s = value as String // TODO: use a safe cast (as?) with an Elvis return instead println(s.uppercase()) // want HELLO }
Show answer Hide answer
val s = value as? String ?: return
println(s.uppercase())

the edges

7. Why won’t this smart-cast?

This fails to compile: a nullable var property name accessed as name.length after a null check. Why, and how do you fix it?

Try it — edit, then press Run fun main() { class User(var name: String?) val user = User("Ada") if (user.name != null) { // Printing user.name.length here won't compile: a var property could // change between the check and the use, so it isn't smart-cast. // TODO: copy user.name into a local val, then print its length. println(user.name) } }
Show answer Hide answer

A var property could change between the check and the use, so the compiler won’t smart-cast it. Copy it into a local val first:

val n = name
if (n != null) println(n.length)

8. After a throw

Given config: Config?, throw IllegalStateException when it’s null; afterward use it as non-null.

Try it — edit, then press Run fun main() { class Config { fun load() = println("loaded") } val config: Config? = Config() if (config == null) return // TODO: throw IllegalStateException("missing config") instead config.load() // config is non-null from here on }
Show answer Hide answer
if (config == null) throw IllegalStateException("missing config")
config.load()           // config is non-null here

9. When smart casts can’t help

You only have a val x: Any, and you’re certain it’s a String, but there’s no check in the code path. Write the explicit cast to call .length.

Try it — edit, then press Run fun main() { val x: Any = "hello" val length = 0 // TODO: cast x to String explicitly and take its length println(length) // want 5 }
Show answer Hide answer
(x as String).length

A plain as throws if you’re wrong; prefer a checked is or a safe as? when you aren’t certain.

10. Combine type and null

Implement lengthOrMinusOne so it returns the length when x is a non-null String, else -1.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun lengthOrMinusOne() { Assert.assertEquals("a String gives its length", 5, lengthOrMinusOne("hello")) Assert.assertEquals("a non-String gives -1", -1, lengthOrMinusOne(42)) Assert.assertEquals("null gives -1", -1, lengthOrMinusOne(null)) } } //sampleStart fun lengthOrMinusOne(x: Any?): Int = if (x is String) 0 else -1 // TODO: return x.length in the String branch //sampleEnd
Show answer Hide answer
fun lengthOrMinusOne(x: Any?) = if (x is String) x.length else -1

A smart cast to String also rules out null, since String is non-nullable.


Going deeper: stability and the fix

12. Early-return narrowing

Use a !is guard with an early return so that, past the guard, x is a String with no cast. Return its length, or -1 when it isn’t a string.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class Test { @Test fun len() { Assert.assertEquals(2, lengthOf("hi")) Assert.assertEquals(-1, lengthOf(42)) } } //sampleStart fun lengthOf(x: Any): Int { // TODO: if x isn't a String, return -1; otherwise return its length return -1 } //sampleEnd
Show answer Hide answer
fun lengthOf(x: Any): Int {
    if (x !is String) return -1
    return x.length          // smart-cast to String from here on
}

The smart cast follows control flow: past the return, the only surviving type is String.

13. Why won’t it compile?

This fails: if (user.name != null) println(user.name.length) where name is a var property. Fix it so it compiles, without changing User.

Implement it, then press Run to check import org.junit.Test import org.junit.Assert class User(var name: String?) class Test { @Test fun len() { Assert.assertEquals(3, nameLength(User("Ada"))) Assert.assertEquals(0, nameLength(User(null))) } } //sampleStart fun nameLength(user: User): Int { // TODO: return user.name's length, or 0 when null — make it compile return 0 } //sampleEnd
Show answer Hide answer
fun nameLength(user: User): Int {
    val name = user.name          // stable local snapshot
    return if (name != null) name.length else 0
}

A mutable property isn’t stable — a custom getter or another thread could change it between the check and the use — so the compiler refuses to smart-cast it. Copy into a local val (or use user.name?.length ?: 0).


Back to the lesson, Kotlin Smart Casts, or on to the next one: null safety.

Comments