Kotlin Help

Type checks and casts

In Kotlin, you can do two things with types at runtime: check whether an object is a specific type, or convert it to another type. Type checks help you confirm the kind of object you're dealing with, while type casts attempt to convert the object to another type.

Checks with is and !is operators

Use the is operator (or !is to negate it) to check if an object matches a type at runtime:

fun main() { val input: Any = "Hello, Kotlin" if (input is String) { println("Message length: ${input.length}") // Message length: 13 } if (input !is String) { // Same as !(input is String) println("Input is not a valid message") } else { println("Processing message: ${input.length} characters") // Processing message: 13 characters } }

You can also use is and !is operators to check if an object matches a subtype:

interface Animal { val name: String fun speak() } class Dog(override val name: String) : Animal { override fun speak() = println("$name says: Woof!") } class Cat(override val name: String) : Animal { override fun speak() = println("$name says: Meow!") } //sampleStart fun handleAnimal(animal: Animal) { println("Handling animal: ${animal.name}") animal.speak() // Use is operator to check for subtypes if (animal is Dog) { println("Special care instructions: This is a dog.") } else if (animal is Cat) { println("Special care instructions: This is a cat.") } } //sampleEnd fun main() { val pets: List<Animal> = listOf( Dog("Buddy"), Cat("Whiskers"), Dog("Rex") ) for (pet in pets) { handleAnimal(pet) println("---") } // Handling animal: Buddy // Buddy says: Woof! // Special care instructions: This is a dog. // --- // Handling animal: Whiskers // Whiskers says: Meow! // Special care instructions: This is a cat. // --- // Handling animal: Rex // Rex says: Woof! // Special care instructions: This is a dog. // --- }

This example uses the is operator to check if the Animal class instance has subtype Dog or Cat to print the relevant care instructions.

You can check if an object is a supertype of its declared type, but it's not worthwhile because the answer is always true. Every class instance is already an instance of its supertypes.

Type casts

To convert the type of an object in Kotlin to another type is called casting.

In some cases, the compiler automatically casts objects for you. This is called smart-casting.

If you need to explicitly cast a type, use as? or as cast operators.

Smart casts

The compiler tracks the type checks and explicit casts for immutable values and inserts implicit (safe) casts automatically:

fun logMessage(data: Any) { // data is automatically cast to String if (data is String) { println("Received text: ${data.length} characters") } } fun main() { logMessage("Server started") // Received text: 14 characters logMessage(404) }

The compiler is even smart enough to know that a cast is safe if a negative check leads to a return:

fun logMessage(data: Any) { // data is automatically cast to String if (data !is String) return println("Received text: ${data.length} characters") } fun main() { logMessage("User signed in") // Received text: 14 characters logMessage(true) }

Control flow

Smart casts work not only for if conditional expressions, but also for when expressions:

fun processInput(data: Any) { when (data) { // data is automatically cast to Int is Int -> println("Log: Assigned new ID ${data + 1}") // data is automatically cast to String is String -> println("Log: Received message \"$data\"") // data is automatically cast to IntArray is IntArray -> println("Log: Processed scores, total = ${data.sum()}") } } fun main() { processInput(1001) // Log: Assigned new ID 1002 processInput("System rebooted") // Log: Received message "System rebooted" processInput(intArrayOf(10, 20, 30)) // Log: Processed scores, total = 60 }

And for while loops:

sealed interface Status data class Ok(val currentRoom: String) : Status data object Error : Status class RobotVacuum(val rooms: List<String>) { var index = 0 fun status(): Status = if (index < rooms.size) Ok(rooms[index]) else Error fun clean(): Status { println("Finished cleaning ${rooms[index]}") index++ return status() } } fun main() { //sampleStart val robo = RobotVacuum(listOf("Living Room", "Kitchen", "Hallway")) var status: Status = robo.status() while (status is Ok) { // The compiler smart casts status to OK type, so the currentRoom // property is accessible. println("Cleaning ${status.currentRoom}...") status = robo.clean() } // Cleaning Living Room... // Finished cleaning Living Room // Cleaning Kitchen... // Finished cleaning Kitchen // Cleaning Hallway... // Finished cleaning Hallway //sampleEnd }

In this example, the sealed interface Status has two implementations: the data class Ok and the data object Error. Only the Ok data class has the currentRoom property. When the while loop condition evaluates to true, the compiler smart casts the status variable to Ok type, making the currentRoom property accessible within the loop body.

If you declare a variable of Boolean type before using it in your if, when, or while condition, any information collected by the compiler about the variable is accessible in the corresponding block for smart-casting.

This can be useful when you want to do things like extract boolean conditions into variables. Then, you can give the variable a meaningful name, which improves your code readability and makes it possible to reuse the variable later in your code. For example:

class Cat { fun purr() { println("Purr purr") } } //sampleStart fun petAnimal(animal: Any) { val isCat = animal is Cat if (isCat) { // The compiler can access information about // isCat, so it knows that animal was smart-cast // to the type Cat. // Therefore, the purr() function can be called. animal.purr() } } fun main(){ val kitty = Cat() petAnimal(kitty) // Purr purr } //sampleEnd

Logical operators

The compiler can perform smart casts on the right-hand side of && or || operators if there is a type check (regular or negative) on the left-hand side:

// x is automatically cast to String on the right-hand side of `||` if (x !is String || x.length == 0) return // x is automatically cast to String on the right-hand side of `&&` if (x is String && x.length > 0) { print(x.length) // x is automatically cast to String }

If you combine type checks for objects with an or operator (||), a smart cast is made to their closest common supertype:

interface Status { fun signal() {} } interface Ok : Status interface Postponed : Status interface Declined : Status fun signalCheck(signalStatus: Any) { if (signalStatus is Postponed || signalStatus is Declined) { // signalStatus is smart-cast to a common supertype Status signalStatus.signal() } }

Inline functions

The compiler can smart-cast variables captured within lambda functions that are passed to inline functions.

Inline functions are treated as having an implicit callsInPlace contract. This means that any lambda functions passed to an inline function are called in place. Since lambda functions are called in place, the compiler knows that a lambda function can't leak references to any variables contained within its function body.

The compiler uses this knowledge, along with other analyses, to decide whether it's safe to smart-cast any of the captured variables. For example:

interface Processor { fun process() } inline fun inlineAction(f: () -> Unit) = f() fun nextProcessor(): Processor? = null fun runProcessor(): Processor? { var processor: Processor? = null inlineAction { // The compiler knows that processor is a local variable and inlineAction() // is an inline function, so references to processor can't be leaked. // Therefore, it's safe to smart-cast processor. // If processor isn't null, processor is smart-cast if (processor != null) { // The compiler knows that processor isn't null, so no safe call // is needed processor.process() } processor = nextProcessor() } return processor }

Exception handling

Smart cast information is passed on to catch and finally blocks. This makes your code safer as the compiler tracks whether your object has a nullable type. For example:

//sampleStart fun testString() { var stringInput: String? = null // stringInput is smart-cast to String type stringInput = "" try { // The compiler knows that stringInput isn't null println(stringInput.length) // 0 // The compiler rejects previous smart cast information for // stringInput. Now stringInput has the String? type. stringInput = null // Trigger an exception if (2 > 1) throw Exception() stringInput = "" } catch (exception: Exception) { // The compiler knows stringInput can be null // so stringInput stays nullable. println(stringInput?.length) // null } } //sampleEnd fun main() { testString() }

Smart cast prerequisites

Smart casts work only when the compiler can guarantee that the variable won't change between the check and its usage. They can be used in the following conditions:

val local variables

Always, except local delegated properties.

val properties

If the property is private, internal, or if the check is performed in the same module where the property is declared. Smart casts can't be used on open properties or properties that have custom getters.

var local variables

If the variable is not modified between the check and its usage, is not captured in a lambda that modifies it, and is not a local delegated property.

var properties

Never, because the variable can be modified at any time by other code.

as and as? cast operators

Kotlin has two cast operators: as and as?. You can use both to cast, but they have different behaviors.

If a cast fails with the as operator, a ClassCastException is thrown at runtime. That's why it's also called the unsafe operator. You can use as when casting to a non-null type:

fun main() { val rawInput: Any = "user-1234" // Casts to String successfully val userId = rawInput as String println("Logging in user with ID: $userId") // Logging in user with ID: user-1234 // Triggers ClassCastException val wrongCast = rawInput as Int println("wrongCast contains: $wrongCast") // Exception in thread "main" java.lang.ClassCastException }

If you use the as? operator instead, and the cast fails, the operator returns null. That's why it's also called the safe operator:

fun main() { val rawInput: Any = "user-1234" // Casts to String successfully val userId = rawInput as? String println("Logging in user with ID: $userId") // Logging in user with ID: user-1234 // Assigns a null value to wrongCast val wrongCast = rawInput as? Int println("wrongCast contains: $wrongCast") // wrongCast contains: null }

To cast a nullable type safely, use the as? operator to prevent triggering a ClassCastException if the cast fails.

You can use as with a nullable type. This allows the result to be null, but it still throws a ClassCastException if the cast is unsuccessful. For this reason, as? is the safer option:

fun main() { val config: Map<String, Any?> = mapOf( "username" to "kodee", "alias" to null, "loginAttempts" to 3 ) // Unsafely casts to a nullable String val username: String? = config["username"] as String? println("Username: $username") // Username: kodee // Unsafely casts a null value to a nullable String val alias: String? = config["alias"] as String? println("Alias: $alias") // Alias: null // Fails to cast to nullable String and throws ClassCastException // val unsafeAttempts: String? = config["loginAttempts"] as String? // println("Login attempts (unsafe): $unsafeAttempts") // Exception in thread "main" java.lang.ClassCastException // Fails to cast to nullable String and returns null val safeAttempts: String? = config["loginAttempts"] as? String println("Login attempts (safe): $safeAttempts") // Login attempts (safe): null }

Up and downcasting

In Kotlin, you can cast objects to supertypes and subtypes.

Casting an object to an instance of its superclass is called upcasting. Upcasting doesn't need any special syntax or cast operators. For example:

interface Animal { fun makeSound() } class Dog : Animal { // Implements behavior for makeSound() override fun makeSound() { println("Dog says woof!") } } fun printAnimalInfo(animal: Animal) { animal.makeSound() } fun main() { val dog = Dog() // Upcasts Dog instance to Animal printAnimalInfo(dog) // Dog says woof! }

In this example, when the printAnimalInfo() function is called with a Dog instance, the compiler upcasts it to Animal because that's the expected parameter type. Since the actual object is still a Dog instance, the compiler dynamically resolves the makeSound() function from the Dog class, printing "Dog says woof!".

You'll often see explicit upcasting in Kotlin APIs where behavior depends on an abstract type. It's also common in Jetpack Compose and UI toolkits, which typically treat all UI elements as supertypes and later operate on specific subclasses:

val textView = TextView(this) textView.text = "Hello, View!" // Upcasts from TextView to View val view: View = textView // Use View functions view.setPadding(20, 20, 20, 20) // Activity expects a View type setContentView(view)

Casting an object to an instance of a subclass is called downcasting. Because downcasting can be unsafe, you need to use explicit cast operators. To avoid throwing exceptions on failed casts, we recommend using the safe cast operator as?, to return null if the cast fails:

interface Animal { fun makeSound() } class Dog : Animal { override fun makeSound() { println("Dog says woof!") } fun bark() { println("BARK!") } } fun main() { // Creates animal as a Dog instance with Animal // type val animal: Animal = Dog() // Safely downcasts animal to Dog type val dog: Dog? = animal as? Dog // Uses a safe call to call bark() if dog isn't null dog?.bark() // "BARK!" }

In this example, animal is declared as type Animal, but it holds a Dog instance. The code safely casts animal to Dog type and uses a safe call (?.) to access the bark() function.

You'll use downcasting in serialization when deserializing a base class to a specific subtype. It's also common when working with Java libraries that return supertype objects, which you may need to downcast in Kotlin.

25 November 2025