skip to content

What does filterIsInstance do, and how does it differ from filter { it is T } plus a manual cast? Why does it need a reified type?

level: seniorimportance: should knowfreq 58%

answer

  1. keeps elements of type R, returns List<R> (no cast)
  2. inline + reified so the is-check works at runtime
  3. JVM erases generics; reified restores R at the call site
  4. filter { it is T } leaves List<Any>, needs extra cast
  5. drops nulls; Class<R> overload + filterIsInstanceTo exist

basics

~20 s

filterIsInstance keeps only the elements that are of a given type and gives you back a list already typed as that type, so you do not have to cast each one yourself. It needs the type known at compile time to do this safely.

solid answer

~40 s

filterIsInstance<R>() returns a List<R> containing only the receiver elements that are instances of R, with the result already narrowed to R — no manual cast needed. It is implemented as an inline function with a reified type parameter, so R is available at runtime to do the is check; without reified, generic type arguments are erased on the JVM and the runtime is-check would be impossible. Compared to filter { it is String }.map { it as String }, filterIsInstance does it in one operator, avoids the redundant cast (smart-cast can't carry across operators), and reads clearer. There's also a filterIsInstance(klass: Class<R>) overload taking a Class token (useful when the type isn't statically known) and filterIsInstanceTo(destination) for writing into an existing collection. Note nullability: filterIsInstance<String>() drops nulls since null isn't an instance of String.

code

kotlin · 6 lines
kotlin
sealed interface Event
data class Click(val x: Int) : Event
data class Key(val code: Int) : Event

val events: List<Event> = listOf(Click(1), Key(65), Click(2))
val clicks: List<Click> = events.filterIsInstance<Click>()  // [Click(1), Click(2)]

go deeper

for a junior

Knows filterIsInstance keeps elements of a type and returns them already typed, avoiding casts.

for a middle

Explains why it beats filter + manual cast and that it drops nulls.

for a senior

Connects it to inline+reified and JVM type erasure, and knows the Class overload and filterIsInstanceTo variant.

for a principal

Articulates the erasure limitation on nested generics, the unchecked-cast warning, and API trade-offs of reified vs Class-token designs.

## What it does `filterIsInstance<R>()` filters a collection down to only the elements that are instances of `R`, **and** returns the result already typed as `List<R>`. ```kotlin val mixed: List<Any> = listOf(1, "two", 3, "four", null) val strings: List<String> = mixed.filterIsInstance<String>() // ["two", "four"] ``` Note `null` is excluded — `null is String` is `false`. To keep nullable strings you'd use a nullable type argument `filterIsInstance<String?>()`. ## Why reified Its signature is roughly: ```kotlin public inline fun <reified R> Iterable<*>.filterIsInstance(): List<R> = filterIsInstanceTo(ArrayList<R>()) ``` - `inline` means the compiler copies the function body into the call site. - `reified` means the type argument `R` is **available at runtime** inside the body. On the JVM, generic type arguments are normally **erased** — at runtime a `List<String>` is just a `List`. A normal generic function therefore *cannot* do `x is R`, because `R` isn't known at runtime. Marking `R` as `reified` (only possible on `inline` functions) makes the compiler substitute the concrete type at each call site, so the `is R` check actually works. ## vs filter + manual cast ```kotlin // verbose, double work: val s1 = mixed.filter { it is String }.map { it as String } // idiomatic: val s2 = mixed.filterIsInstance<String>() ``` With `filter { it is String }` the result type stays `List<Any>` — the smart-cast inside the lambda doesn't propagate to the outer result, so you still need a `map { it as String }`. `filterIsInstance` collapses both steps and narrows the type for you in a single pass. ## Overloads and variants - `filterIsInstance(klass: Class<R>)` — non-reified overload taking a `Class` token; use when the type is only known at runtime (e.g. passed in dynamically). - `filterIsInstanceTo(destination)` — writes matching elements into an existing `MutableCollection`. ## Gotchas - **Erasure of the argument's own generics**: `filterIsInstance<List<String>>()` can only check that elements are a `List` (the inner `String` is erased), so it may admit a `List<Int>`. The compiler warns about this. - Nulls are dropped unless you ask for a nullable type.

  • Why can't a normal (non-inline) generic function do `x is R`?
    Because JVM generics are erased: at runtime R is unknown. Only inline functions with a reified type parameter let the compiler inline the concrete type at the call site, making the is-check legal.
  • What's the limitation of filterIsInstance<List<String>>()?
    Erasure means the runtime check can only confirm the element is a List; the String type argument is erased, so a List<Int> would also pass. The compiler warns this check is unchecked.

saying these in an interview costs you the question

  • Saying the result is still List<Any> and needs casting
  • Not knowing reified requires inline
  • Claiming generic checks work without reification on the JVM
  • Forgetting that filterIsInstance<String>() drops nulls
  • Asserting filterIsInstance<List<String>>() fully checks the inner type

context