skip to content

How does `filterIsInstance<T>()` work, and why couldn't you write its exact signature without `reified`?

level: middleimportance: should knowfreq 45%

answer

  1. Keeps elements where element is T
  2. is T needs reified (erased type param otherwise)
  3. Non-reified overload takes Class<R> and uses isInstance
  4. filterIsInstance<List<String>>() can only check List
  5. reified retains top-level type, not nested args

basics

~10 s

filterIsInstance<T>() keeps only the elements that are of type T. It needs reified because, without the real type at runtime, the function couldn't perform the is T check on each element.

solid answer

~40 s

`filterIsInstance` is declared roughly as `inline fun <reified R> Iterable<*>.filterIsInstance(): List<R>`. Internally it iterates and keeps elements where `element is R`, returning a `List<R>`. The `is R` check is only legal because `R` is `reified`: when the call is inlined, `R` is replaced by the concrete type (e.g., `String`), so the bytecode does `element is String`. Without `reified` you'd hit "Cannot check for instance of erased type: R". The non-reified alternative is the overload `filterIsInstance(klass: Class<R>)` (Kotlin also has a `KClass`-style approach), which passes the class explicitly so erasure isn't an obstacle. Caveat: because of erasure, `filterIsInstance<List<String>>()` can only test for `List` — the element type argument can't be checked, so it would keep any `List`.

code

kotlin · 6 lines
kotlin
// Reified version (no explicit class needed)
val ns = listOf<Any>(1, "x", 2L, 3).filterIsInstance<Int>() // [1, 3]

// Hand-rolled non-reified equivalent
fun <R> Iterable<*>.onlyOf(klass: Class<R>): List<R> =
    mapNotNull { if (klass.isInstance(it)) klass.cast(it) else null }

go deeper

for a junior

Knows what filterIsInstance does and that it needs the type at runtime.

for a middle

Can sketch the signature, explain why is R needs reified, and recall the Class-based alternative.

for a senior

Articulates the nested-generics erasure limitation and the isInstance/cast pattern for dynamic code.

for a principal

Discusses API ergonomics of reified vs class-parameter overloads and the pitfalls of exposing reified-only filtering in libraries that need reflective entry points.

## What it does `filterIsInstance<T>()` returns a new list containing only elements assignable to `T`: ```kotlin val mixed: List<Any> = listOf(1, "a", 2, "b", 3.0) val strings: List<String> = mixed.filterIsInstance<String>() // ["a", "b"] ``` ## The signature and why reified is required Simplified stdlib definition: ```kotlin public inline fun <reified R> Iterable<*>.filterIsInstance(): List<R> { return filterIsInstanceTo(ArrayList<R>()) } public inline fun <reified R, C : MutableCollection<in R>> Iterable<*>.filterIsInstanceTo(destination: C): C { for (element in this) if (element is R) destination.add(element) return destination } ``` The line `if (element is R)` is the crux. A type check `x is R` against a **type parameter** is illegal in Kotlin because, after JVM erasure, `R` doesn't exist at runtime. Marking `R` as `reified` (allowed because the function is `inline`) makes the compiler substitute the concrete type at the call site, turning `element is R` into, e.g., `element is String`. That's a legal, real type check. ## The non-reified alternative The stdlib also provides an overload that takes the class explicitly: ```kotlin public fun <C : MutableCollection<in R>, R> Iterable<*>.filterIsInstanceTo(destination: C, klass: Class<R>): C ``` Here you pass `String::class.java`, so the function uses `klass.isInstance(element)` instead of `element is R`. No `reified` needed because the type is provided as a value, not as an erased type parameter. This is the pattern Java-interop and dynamic code use. ## Erasure limitation you must mention ```kotlin val lists: List<Any> = listOf(listOf("x"), listOf(1), "nope") lists.filterIsInstance<List<String>>() // [["x"], [1]] — keeps BOTH lists! ``` Because the element type of `List` is itself erased, `is List<String>` can only check for `List`. `reified` retains the **top-level** type `R`, not the type arguments nested inside an element's runtime value. So `filterIsInstance<List<String>>()` will retain any `List`, not just lists of strings.

  • What does `listOf<Any>(listOf(1), listOf("a")).filterIsInstance<List<Int>>()` return?
    Both lists. Erasure means the check is effectively `is List`, so the inner `Int`/`String` element type can't be tested; every `List` passes.
  • How would you implement `filterIsInstance` for code that only knows the class at runtime?
    Pass a `Class<R>` (or `KClass<R>`) parameter and use `klass.isInstance(element)` then `klass.cast(...)`. No reified needed since the type comes in as a value.

saying these in an interview costs you the question

  • Believing filterIsInstance<List<String>>() filters by element type
  • Not knowing the is T check is what needs reified
  • Claiming there's no non-reified alternative
  • Saying it mutates the original list rather than returning a new one
  • Confusing it with filterNotNull or filter { it is T }

context