Explain how `is` interacts with nullable types and how negative `is`/null checks produce smart casts via control flow.
answer
- is T excludes null; is T? includes null
- fail-and-return narrows the rest (negative smart cast)
- control-flow analysis, not just if-body
- && / || propagate narrowing
- requireNotNull/require contracts narrow too
basics
~20 sx 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.
solid answer
~50 s`is` on a nullable: `x: String?` and `x is String` returns `false` for `null` and smart-casts `x` to non-null `String` in the positive branch. `x is String?` matches null too. Crucially, Kotlin performs **flow-based (negative) smart casts**: a check that, if it fails, exits the scope, narrows the variable afterward. `if (x !is String) return`/`throw` makes `x` a `String` for the remainder; `if (x == null) return` narrows `T?` to `T`. The same works with `&&`/`||`: in `x is String && x.length > 0`, `x` is `String` in the second operand; in `x !is String || x.isEmpty()` the right operand sees `String`. The compiler builds this from control-flow analysis, requiring the value to be stable. Combine `is` and null checks freely: `if (x != null && x is List<*>) ...`. The Elvis operator and `requireNotNull`/`checkNotNull` (which have `Nothing`-returning contracts) also trigger narrowing.
code
kotlin · 5 linesfun process(x: Any?): Int {
if (x == null || x !is String) return -1 // exit unless non-null String
// x smart-cast to String below
return x.length
}go deeper
Knows is String excludes null and that you can check with is String? to include it.
Uses early return/Elvis to narrow and knows &&/|| propagate the cast.
Explains control-flow-based negative smart casts and stdlib contract functions like requireNotNull.
Reasons about how Kotlin contracts and definite-assignment/flow analysis underpin narrowing and designs APIs with contracts for ergonomic call sites.
## `is` and nullability For a nullable variable `x: Any?`: - `x is String` → `false` when `x == null`; in the true branch `x` is smart-cast to non-null `String`. - `x is String?` → also matches `null`; smart-casts to `String?`. - `x !is String` → true for null and for non-Strings. ```kotlin fun f(x: Any?) { if (x is String) x.length // x: String (non-null) if (x is String?) { /* x: String? */ } } ``` ## Negative smart casts (control-flow narrowing) Kotlin's smart cast is driven by **control-flow analysis**, not just `if` bodies. If a branch that *fails* the desired condition leaves the scope, the variable is narrowed in the code that follows: ```kotlin fun len(x: Any?): Int { if (x !is String) return 0 // leave if NOT a String return x.length // x: String here } fun nn(x: String?): Int { x ?: return 0 // Elvis: leave if null return x.length // x: String (non-null) } ``` Because the only way to reach the trailing code is when the check held, the compiler narrows the type. `throw`, `return`, `continue`, `break`, and calls returning `Nothing` (like `error(...)`) all serve as the "exit". ## Boolean short-circuit narrowing Within `&&`/`||`, narrowing flows between operands: ```kotlin if (x is String && x.length > 3) { } // 2nd operand: x is String if (x !is String || x.length == 0) return // right operand: x is String ``` The rule: in `a && b`, `b` is evaluated only when `a` held; in `a || b`, `b` only when `a` failed — the compiler narrows accordingly. ## Contracts and stdlib helpers Functions like `requireNotNull(x)`, `checkNotNull(x)`, and `require(x is T)` carry **contracts** so the compiler smart-casts after them: ```kotlin fun g(x: Any?) { requireNotNull(x) // contract: returns => x != null require(x is String) // contract: returns => x is String x.length // x: String } ``` ## Stability still applies All of the above needs the value to be a stable `val`/local. An unstable property won't smart-cast even with a negative check — snapshot it first. ## Summary - `is T` excludes null; `is T?` includes it. - Failing checks that exit the scope narrow the rest (negative smart cast). - `&&`/`||` propagate narrowing between operands. - Elvis `?:` with `return`/`throw`, and contract-bearing stdlib functions, also narrow.
- After `requireNotNull(x)`, why is `x` non-null below?`requireNotNull` declares a Kotlin contract `returns() implies (x != null)`; if it returns normally the compiler knows `x` can't be null and narrows accordingly.
- In `x !is String || x.length == 0`, why does the right side compile?The right operand of `||` runs only when the left was false, i.e. `x` IS a String, so it's smart-cast there.
saying these in an interview costs you the question
- Saying `x is String` is true for null
- Not recognizing that early `return`/`throw` enables smart casts afterward
- Thinking smart casts only happen inside an `if` body
- Forgetting stability still applies to negative smart casts