A type check asks what kind of value you actually have. A cast asks Kotlin to view that value through a more specific type. These operations matter when an API returns Any, when several subclasses share a base interface, or when you validate mixed external data. They do not convert the value into an unrelated representation.
The null-safety lesson introduces smart casts after null checks. This reference explains type narrowing more fully, including safe casts, mutable-property limits and erased generic arguments. You will build a parser that accepts a deliberately small set of representations and rejects unsupported shapes instead of forcing every input through an unsafe cast.
is narrows a known relationship
Use is to test whether a value belongs to a type, and !is for the opposite. Inside a branch where the compiler can prove that relationship, it can allow the type's operations without a separate cast. For example, a String check lets you read length on a value previously typed Any.
Smart casts also work after an early return. If a function returns when a value is not a String, the remaining path can treat it as a String when stability rules permit. This keeps validation code readable: reject the unsupported case first, then work with the accepted type.
A type check is not a domain check. A String can still be too long, blank or malformed. An Int can still be negative. Narrow the representation first, then apply the format and range rules required by the operation. That order avoids calling string methods on array-shaped input while preserving clear business validation.
as and as? have different failure policies
As performs a cast that fails when the actual value does not satisfy the requested type. It is appropriate only when the relationship is guaranteed by a trusted contract and a violation represents a programming error. As? returns null when the cast cannot succeed, making it suitable for an explicitly optional narrowing step.
Neither operator parses a numeric string. Casting the String "42" to Int does not turn it into forty-two. Use toIntOrNull for that conversion, then validate its allowed range. Likewise, casting a domain object to a sibling type does not transform its fields into another model.
A safe cast returning null can conflate an original null with a wrong type if both are possible. If your endpoint must distinguish missing from malformed input, test those cases separately. Convenient syntax should not erase distinctions that matter to a validation response or audit record.
Smart casts require stable evidence
The compiler must know that the value cannot change between the check and use. A local val is usually straightforward. A mutable property, an open property or a property with a custom getter may produce a different value on the next read, so checking it once does not always make a later access safe.
Store the value in a local snapshot when that expresses the intended operation. Checking snapshot and then using snapshot gives the compiler and reader one stable value. It also makes the timing explicit if the original property can be updated by another component.
Do not add double-bang assertions simply to overcome a failed smart cast. The compiler may be identifying a real uncertainty. A local variable, safe call or carefully structured branch is usually clearer. Concurrency safety is broader than smart-cast acceptance, so a compiler-approved local read does not automatically make shared mutation synchronized.
Any, null and generic erasure
Any represents non-null values at the broad language level, while Any? also permits null. Use a more specific interface when the operation needs a behavior such as formatting or calculating a total. A collection of Any values forces repeated runtime decisions that a typed model can avoid.
Generic arguments are usually erased at runtime. A check for List<String> is not a general way to validate arbitrary external lists. Check List<*> first, then validate each element if the required shape is a list of strings. The original generics lesson covers reified helpers, but even those do not magically verify every nested generic element.
The official type-check and cast guide explains narrowing and stability rules. Those rules evolve with compiler analysis, so keep code explicit rather than depending on the newest inference behavior for a project supporting older compilers. The examples use established forms that express the same contract clearly.
Kotlin Type Checks, Safe Casts and Smart Casts Examples
Each example shows the code, the output it produces and what each part does.
Branch on actual runtime types
Each branch sees a narrower type. The function deliberately supports only text and integers even though Any can hold many other values. That small contract is easier to test than an implicit rule that tries to interpret every object as useful data.
fun describe(value: Any?): String = when (value) {
null -> "missing"
is String -> "text:${value.length}"
is Int -> "integer:$value"
else -> "unsupported"
}
fun main() {
println(describe("Pen"))
println(describe(4))
println(describe(null))
}text:3
integer:4
missingKotlin Type Checks, Safe Casts and Smart Casts in Real Projects
Worked exercise: parse a bounded quantity
Accept either an Int or an integer-formatted String as a quantity. Everything else is unsupported. After selecting the representation, require a value from one through fifty. Return null for rejected input so the caller can produce a field-level validation response. The result type makes failure visible without claiming every numeric-looking value is valid.
fun quantity(value: Any?): Int? {
val parsed = when (value) {
is Int -> value
is String -> value.toIntOrNull()
else -> null
}
return parsed?.takeIf { it in 1..50 }
}
fun main() {
println(quantity("12"))
println(quantity(4))
println(quantity("1.5"))
println(quantity(listOf(3)))
}12
4
null
nullThe parser does not silently truncate a decimal or accept a list containing one number. Try zero and fifty-one to test both range boundaries. If absence needs a different response from malformed input, replace the nullable result with a sealed validation outcome containing distinct cases. A narrow type check and an explicit range provide a better contract than unchecked casting.
Kotlin Type Checks, Safe Casts and Smart Casts FAQ
Does a cast convert a numeric string into a number?
No. A cast changes the type view only when the object already satisfies that type relationship. A String remains a String even when its characters look numeric. Use a parsing function such as toIntOrNull for conversion, then enforce the permitted range. This distinction prevents accidental acceptance of decimals, malformed text or oversized values. It also gives you a predictable failure result that can be translated into a useful input error rather than a runtime cast exception.
Why will Kotlin not smart-cast my property?
The property may not be stable between reads. A mutable, open or custom-getter property can change after the check, so a later access cannot safely assume the earlier type. Read it into a local val when a snapshot fits the operation, then check and use that snapshot. Do not force the compiler with an unsafe assertion unless a real invariant guarantees the relationship. The warning often points to a design or lifetime assumption worth making explicit.
Is as? enough to validate a request field?
It validates only one type relationship and reports a failed cast as null. A successfully cast string may still be blank, too long or incorrectly formatted. A successfully cast integer may violate the domain range. It can also merge missing input and wrong-type input into one result. Build validation in layers: distinguish relevant presence states, check shape, parse the intended representation and enforce the business rule before calling domain operations.
Can I check whether an unknown list contains only strings?
Check that it is a List<*> and examine every element under the desired contract. Runtime generic erasure means a simple List<String> type test is not generally available for arbitrary input. Decide whether null elements are permitted and whether an empty list is valid. If the check succeeds, construct a typed result through a controlled transformation instead of scattering unchecked casts. This is especially useful at JSON and plugin boundaries where the outer list alone does not guarantee its element types.
Kotlin Type Checks, Safe Casts and Smart Casts Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
An unknown value must be a list containing only strings. Which plan is appropriate?
Kotlin Type Checks, Safe Casts and Smart Casts Quiz
2 questions to check what you learned.
Which operation parses the text "42" as an optional integer?
- value as Int
- value.toIntOrNull()
- value is Int while it remains a String
Why can a custom-getter property resist a smart cast?
- Kotlin forbids every property check
- Getters always return null
- A later read may not have the value checked earlier