After a check such as x != null or x is String, the compiler smart casts the variable to the narrower type inside the branch, provided the value cannot change in between. A var property of a class can, so if (line.note != null) line.note.length fails with "smart cast to 'String' is impossible, because 'note' is a mutable property that could be mutated concurrently"; copy it into a local val first.
A value from Java, whose types carry no ?, arrives as a platform type, shown as String!: Kotlin lets you treat it either way, at your own risk. The JDK's unannotated System.getenv() shows it:
fun describe(x: Any?): String = when (x) {
null -> "nothing"
is String -> "text of ${x.length} chars" // x is smart-cast to String here
is Int -> "a number, doubled ${x * 2}"
else -> "a ${x::class.simpleName}"
}
fun main() {
println(listOf(null, "Gift wrap", 41, 2.5).map(::describe))
val api: String? = System.getenv("BOOKNEST_API") // declared nullable: safe
println(api?.length ?: "BOOKNEST_API is not set")
val raw = System.getenv("BOOKNEST_API") // String!: compiles unchecked
println(raw.length)
}[nothing, text of 9 chars, a number, doubled 82, a Double] BOOKNEST_API is not set Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "raw" is null at SmartKt.main(smart.kt:12) at SmartKt.main(smart.kt)
Give Java values an explicit Kotlin type on entry, as api does. Kotlin also reads JSpecify, JetBrains and Android nullability annotations, so an annotated @Nullable String arrives as String?.