skip to content

Type Checks: is / !is

is and !is perform runtime type checks and, in the same breath, narrow the variable's static type for the rest of the branch. Combined with when, this is how idiomatic Kotlin dispatches on type without a cast in sight.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

What do the `is` and `!is` operators do in Kotlin, and what is a smart cast?

level: juniorimportance: must knowfreq 85%

answer

  1. is = runtime check + smart cast
  2. !is is the negation
  3. smart cast narrows static type, no `as` needed
  4. needs a STABLE value (val / unmutated local var)
  5. works in if, &&/||, early return, when

basics

~20 s

is checks at runtime whether a value is of a given type; !is is the negation. After a successful is check, Kotlin automatically treats the variable as that type, so you can use it without an extra cast. That automatic narrowing is a smart cast.

solid answer

~40 s

`is` is Kotlin's runtime type-check operator: `x is String` returns `true` if `x` is a `String` (or subtype). `!is` is the inverse. Unlike Java's `instanceof`, a successful `is` triggers a smart cast: the compiler narrows the variable's static type within the scope where the check is guaranteed to hold, so you can call `String` members directly without writing `x as String`. Smart casts work after `is`, after `!= null` (narrowing `T?` to `T`), and combine with `&&`, `||`, early returns, and `when` branches. They require the value to be stable — typically a `val`, or a `var` local the compiler can prove isn't mutated between the check and use. They do not apply to mutable properties from other modules or `var`s captured by closures.

code

kotlin · 5 lines
kotlin
fun area(shape: Any): Double = when (shape) {
    is Circle -> Math.PI * shape.radius * shape.radius // smart-cast to Circle
    is Rect -> shape.w * shape.h                       // smart-cast to Rect
    else -> 0.0
}

go deeper

for a junior

Knows is/!is check the type and that a smart cast lets you use the variable without an explicit cast.

for a middle

Explains the stability requirement and that smart casts also apply to null checks and inside when.

for a senior

Distinguishes stable vs unstable values, negative smart casts via early return, and erasure limits on generics.

for a principal

Reasons about thread-safety/getter implications behind the stability rule and guides API/property design to keep values smart-castable.

## The `is` / `!is` operators `is` is Kotlin's **runtime type check**. `value is Type` evaluates to `true` when `value`'s actual runtime class is `Type` or a subtype of it. `!is` is the negation (`value !is Type` == `!(value is Type)`). It is the rough equivalent of Java's `instanceof`, but with a key extra power. ```kotlin fun describe(x: Any): String { if (x is String) { // inside this block, x is statically a String return "string of length ${x.length}" } return "not a string" } ``` ## Smart casts A **smart cast** is the compiler automatically narrowing a variable's *static* (compile-time) type after it has proven the runtime type. After `if (x is String)`, inside the `if` body `x` is typed as `String`, so `x.length` compiles with no explicit cast (no `as`). Smart casts trigger from several conditions: - `is` checks: `if (x is Foo) { x.fooMethod() }` - null checks: `if (x != null) { x.member }` narrows `Foo?` to `Foo` - combined boolean logic with `&&` / `||` - early `return`/`throw` that exclude a type (negative smart cast) ```kotlin fun len(x: Any?): Int { if (x !is String) return 0 // x is smart-cast to String here (after the negative check returns) return x.length } fun combo(x: Any) { if (x is String && x.length > 0) { /* x is String in the 2nd operand */ } if (x !is String || x.length == 0) return } ``` ## Stability requirement The compiler only smart-casts a **stable** value it can guarantee won't change between check and use: - local `val`s — always smart-castable - local `var`s — smart-castable if not modified between check and use, and not captured by a modifying closure - private/internal `val` properties with a backing field It **cannot** smart-cast: - `var` properties (could be changed by another thread or custom getter) - `open`/overridable properties or properties with custom getters (the getter could return different values each call) - properties from other modules For those cases, capture into a local `val` first, or use `?.let { }`. ## `is` inside `when` `when` branches support `is` directly and smart-cast inside each branch: ```kotlin when (x) { is Int -> x + 1 // x: Int is String -> x.length // x: String !is Boolean -> 0 else -> -1 } ``` With a `sealed` hierarchy, the compiler can verify exhaustiveness so `else` may be unnecessary. Note: generic type arguments are erased, so `x is List<String>` is not allowed (only `x is List<*>`).

  • Why can't a public `var` property be smart-cast?
    Between the check and the use it could be reassigned (e.g. by another thread or via a custom getter), so the compiler can't guarantee the narrowed type still holds.
  • How is `is` different from Java's `instanceof`?
    `instanceof` only returns a boolean; you still cast manually. Kotlin's `is` additionally smart-casts the variable, removing the explicit cast.

An is check is like checking a parcel's label; once verified, you can handle it as that kind of parcel without re-checking each time.

saying these in an interview costs you the question

  • Saying `is` performs a cast like `as` (it only checks; smart cast is automatic narrowing)
  • Claiming smart cast works on any `var` property
  • Confusing `!is` with `!=`
  • Thinking `is` can check generic type arguments like `List<String>`

context

open as a page

Give cases where Kotlin refuses to smart-cast after an `is`/null check, and how you work around each.

level: middleimportance: must knowfreq 70%

basics

~20 s

Kotlin won't smart-cast when the value could change between check and use: mutable var properties, properties with custom getters, open properties, and values from other modules. Copy the value into a local val (or use ?.let) and check that instead.

open as a page

How do `is`-checks behave inside a `when` expression, and how does that interact with `sealed` exhaustiveness?

level: middleimportance: should knowfreq 65%

basics

~20 s

In when, each is Type -> branch smart-casts the subject to that type inside the branch. With a sealed class or enum subject used as an expression, the compiler can require all subtypes to be covered, so no else branch is needed.

open as a page

Why does `x is List<String>` not compile, and what is the correct way to type-check a generic at runtime?

level: seniorimportance: should knowfreq 55%

basics

~20 s

Generic type arguments are erased at runtime, so the JVM can't tell a List<String> from a List<Int>. Kotlin forbids is List<String>. Use a star projection is List<*> to check only that it's a list, then verify elements individually if needed.

open as a page

Explain how `is` interacts with nullable types and how negative `is`/null checks produce smart casts via control flow.

level: seniorimportance: should knowfreq 50%

basics

~20 s

x is String is true only for non-null Strings; for a nullable you can write is String? to include null. Kotlin also smart-casts via control flow: after if (x !is String) return, the rest of the function treats x as String, because the only way past is when the check held.

open as a page