skip to content

When does a generic-receiver extension need `reified`, and how would you write `fun <reified T> Any?.castOrNull(): T?`-style helpers correctly?

level: seniorimportance: should knowfreq 40%

answer

  1. Generics erased → can't do `is T`/`T::class`
  2. reified requires `inline fun`
  3. Enables `as? T`, `is T`, `T::class`
  4. Can't instantiate `T()` with reified
  5. stdlib filterIsInstance / fromJson pattern

basics

~20 s

You need reified only when the function must know the real type at runtime — to do is T, as? T, or T::class. The function must be inline. Then you can write helpers that safely cast or filter by type.

solid answer

~50 s

Normal generics are **erased** at runtime, so you cannot write `is T` or `T::class` against a plain type parameter. Marking the parameter `reified` (only allowed on an `inline fun`) keeps the type available at the call site, where the compiler inlines it and substitutes the concrete type — enabling `value as? T`, `value is T`, and `T::class`. For a receiver extension you combine `inline` + `reified`, e.g. `inline fun <reified T> Any?.castOrNull(): T? = this as? T`. The receiver here is `Any?` (nullable) so `this` may be null; `as?` returns null for both a null receiver and a type mismatch. Constraints: reified params can't be used to instantiate `T()` (no constructor known), can't be passed where a non-reified type param is expected, and force the function to be inline (so it can't be virtual/abstract). Common real uses: `filterIsInstance<T>()`, Gson/Moshi `fromJson`, `getSystemService`.

code

kotlin · 12 lines
kotlin
inline fun <reified T> Any?.castOrNull(): T? = this as? T

inline fun <reified T> List<Any?>.onlyOfType(): List<T> =
    filterIsInstance<T>()

fun main() {
    val items: List<Any?> = listOf(1, "a", 2, null, "b")
    println(items.onlyOfType<String>()) // [a, b]
    val v: Any? = 7
    println(v.castOrNull<String>())     // null
    println(v.castOrNull<Int>())        // 7
}

go deeper

for a junior

Aware that runtime type info is normally lost and that reified exists for type checks.

for a middle

Can write an inline reified extension to do is T/as? T and knows it must be inline.

for a senior

Explains erasure, the inline requirement, the can't-instantiate limit, and applies the pattern (filterIsInstance, fromJson) on nullable receivers.

for a principal

Weighs inlining cost/binary-size, API ergonomics of reified helpers vs passing KClass, and erasure interop concerns across module/Java boundaries.

## The problem: type erasure The JVM erases generic type arguments at runtime. So inside a generic function, a plain `T` is unknown at runtime: ```kotlin fun <T> Any?.castOrNull(): T? = this as? T // WARNING: unchecked cast, T not really checked ``` This compiles with an *unchecked cast* warning and does **not** actually verify the type — `as? T` erases to `as? Any?`, so it never returns null on mismatch. Broken. ## The fix: reified `reified` makes the type argument available at runtime. Rules: - Only on a parameter of an **`inline fun`** (the body is copied to the call site, where the concrete type IS known). - Then you may use `T::class`, `is T`, `as? T` correctly. ```kotlin inline fun <reified T> Any?.castOrNull(): T? = this as? T val x: Any? = "hello" println(x.castOrNull<String>()) // hello println(x.castOrNull<Int>()) // null ✅ now really type-checked ``` Because the receiver type is `Any?` (**nullable**), `this` can be null; `as?` yields null for a null receiver too, which is the desired behavior. ## A type-filtering helper ```kotlin inline fun <reified T> Iterable<*>.firstInstanceOrNull(): T? { for (e in this) if (e is T) return e return null } ``` The stdlib `filterIsInstance<T>()` is built exactly this way. ## What reified cannot do - **Cannot** call `T()` — the constructor isn't known. Pass a factory lambda or `() -> T` instead. - **Cannot** be used by a non-inline, `open`, `abstract`, or `virtual` function — reified requires inlining. - **Cannot** forward `T` to another non-reified generic without itself being reified. - Using `T::class` gives a `KClass<T>`; `T::class.java` gives the Java `Class`. ## When you do NOT need reified If the body never inspects `T`'s runtime type — like `also`, `apply`, `let`, `ifNull` — a plain type parameter suffices. Reify only for `is`/`as?`/`::class`. ## Practical examples - `inline fun <reified T> Gson.fromJson(json: String): T = fromJson(json, T::class.java)` - `inline fun <reified T> Bundle.require(key: String): T = get(key) as T` - `inline fun <reified T : Any> Context.service(): T = getSystemService(T::class.java)`

  • Why must a reified type parameter's function be `inline`?
    Inlining copies the body to each call site, where the concrete type argument is known and substituted. Without inlining the type would be erased and `is T`/`T::class` impossible.
  • Can you write `fun <reified T> create(): T = T()`?
    No. Even reified, the constructor of an arbitrary T is unknown. Pass a factory (`factory: () -> T`) or use reflection (`T::class.constructors`) deliberately.
  • Without reified, what does `this as? T` actually do?
    It erases to `as? Any?` (or the bound), so it never returns null on a type mismatch — an unchecked, ineffective cast that the compiler warns about.

Erased generics are like a sealed box with the label removed; reified re-prints the label at the moment you open it (inline at the call site) so you can sort by contents.

saying these in an interview costs you the question

  • Using `is T`/`T::class` without `reified`
  • Marking reified on a non-inline function
  • Trying to instantiate `T()` from a reified param
  • Claiming `as? T` works for type-checking without reified
  • Forgetting reified can't be passed to non-reified generics

context