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 ascast 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
}
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:
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.