skip to content

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