Why does `x is List<String>` not compile, and what is the correct way to type-check a generic at runtime?
answer
- JVM erases type arguments
- is List<String> forbidden
- is List<*> = star projection allowed
- check elements with all { it is X }
- reified + inline = real generic is-check
basics
~20 sGeneric 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.
solid answer
~50 sOn the JVM, generic type arguments are **erased**, so at runtime a `List<String>` and `List<Int>` are both just `List`. Because an `is` check happens at runtime, Kotlin **rejects** `x is List<String>` — it can't be verified. You may only check the erased shape with a **star projection**: `x is List<*>`, which confirms it's some `List` without claiming the element type. To know the element type you must inspect elements (`(x as List<*>).all { it is String }`) or carry the type yourself. For functions, a **`reified`** type parameter on an `inline fun` preserves the type token, letting you write `x is T` inside the function — the only way to do a generic `is` check, since `inline` + `reified` substitutes the concrete type at the call site. After `is List<*>`, the smart cast is to `List<*>` (element type stays `Any?`).
code
kotlin · 6 linesinline fun <reified T> Collection<*>.firstOfType(): T? {
for (e in this) if (e is T) return e // reified makes `is T` legal
return null
}
val n: Int? = listOf("a", 1, "b").firstOfType<Int>() // 1go deeper
Knows you can't write is List<String> and that is List<*> is used instead.
Explains erasure as the reason and checks element types by iterating.
Uses inline + reified to perform genuine generic is T checks and understands the unchecked-cast tradeoff.
Designs generic APIs around erasure (reified helpers, type tokens) and weighs reflection vs. reification for runtime type decisions.
## Type erasure Kotlin runs on the JVM, which uses **type erasure**: generic type arguments exist only at compile time and are removed in bytecode. At runtime there is one `List` class — `List<String>` and `List<Int>` are indistinguishable. Since `is` is a **runtime** check, the compiler can't honor `x is List<String>` and rejects it: ```kotlin // if (x is List<String>) ... // ERROR: cannot check for instance of erased type ``` ## Star projection: `is List<*>` You can check the **erased** type with a star projection, which means "a `List` of some unknown element type": ```kotlin if (x is List<*>) { // x: List<*> — elements are typed Any? val first = x.firstOrNull() } ``` The smart cast narrows to `List<*>`, not `List<String>` — element access yields `Any?`. ## Checking element types If you genuinely need the element type, inspect elements at runtime: ```kotlin @Suppress("UNCHECKED_CAST") fun asStringList(x: Any): List<String>? = if (x is List<*> && x.all { it is String }) x as List<String> else null ``` The final cast is unchecked (the compiler warns) because erasure can't verify it — you've manually established the invariant. ## `reified` type parameters The only way to do a real generic `is` check is an **`inline fun`** with a **`reified`** type parameter. `inline` copies the function body to each call site, and `reified` substitutes the concrete type argument there, so `T` is known at runtime: ```kotlin inline fun <reified T> Any?.isType(): Boolean = this is T val ok = "hi".isType<String>() // compiles to: "hi" is String ``` `reified` requires `inline`; it can't be used on regular (non-inline) functions or on class type parameters. ## Exception: fully-star or self-evident cases `x is Array<*>` is allowed; checking a concrete array element type at runtime still isn't (except primitive arrays like `IntArray`, which are distinct JVM types). For nested generics the same erasure rule applies. ## Summary - `is ConcreteGeneric<Arg>` → forbidden (erasure). - `is Generic<*>` → allowed; smart-casts to the star-projected type. - Element checks → iterate and check each element (`it is X`). - Generic `is T` → only via `inline fun <reified T>`.
- Why must `reified` parameters be on `inline` functions?Reification works by substituting the concrete type at each call site during inlining; without inlining there is no call site to substitute into, so the type would still be erased.
- What does smart cast give you after `x is List<*>`?It narrows `x` to `List<*>`; you can call `List` members, but elements are typed `Any?` since the element type remains unknown.
Erasure is like shipping boxes whose contents labels are torn off at the door — you can tell it's a box (List<*>) but not what's inside without opening it.
saying these in an interview costs you the question
- Believing `x is List<String>` compiles and works
- Thinking `reified` works on normal (non-inline) functions
- Not knowing star projection `<*>` is the allowed form
- Claiming the JVM keeps generic type arguments at runtime