Reified Type Parameters

Ordinary generic functions in Kotlin (and Java) lose track of their type argument once the program is running — the JVM erases it during compilation. That means a plain generic function can’t ask "is this value actually a T?" or look up T‘s class, because by the time that code runs, T simply isn’t there anymore. Reified type parameters are a Kotlin-only feature that gets around this for a specific, useful class of functions. This lesson explains exactly why erasure normally blocks this, how the inline + reified combination works around it at compile time, and where it genuinely earns its keep.

Overview / How it works

When you compile List<String> and List<Int> on the JVM, both end up as the exact same class file at runtime: List. The type argument is a compile-time-only concept used for checking your code; it is erased before the bytecode is generated. This is called type erasure, and Kotlin inherits it because it targets the JVM. As a direct consequence, inside an ordinary generic function like fun <T> check(value: Any): Boolean, the compiler refuses to let you write value is T, because at runtime there is no T left to check against — every call to check, regardless of what type argument was used, shares one compiled method body that has no idea what T was.

Kotlin offers a way around this, but only for inline functions. When you mark a function inline, the compiler doesn’t generate a normal callable method at all — instead, it copies the function’s bytecode directly into every call site, as if you had pasted the code there by hand. If you additionally mark a type parameter reified, the compiler goes one step further: at each call site it knows the concrete type argument you used (e.g. check<String>(...)), so while inlining it substitutes that concrete type everywhere T appears in the body. The inlined copy at that call site no longer has an abstract T at all — it has String, written directly into the generated bytecode. That is why value is T becomes legal inside a reified inline function: by the time the compiler emits bytecode, T has already been replaced with a real, erasure-proof type.

This unlocks several operations that are otherwise impossible on a type parameter: value is T and value as T (runtime type checks and casts), T::class and T::class.java (getting the class object, useful for reflection or Java interop), and creating a properly typed array with arrayOfNulls<T>(size). None of this is JVM magic — it’s a compile-time trick. Type erasure still applies everywhere else in the JVM; reified just lets specific inlined call sites route around it locally.

Syntax

inline fun <reified T> functionName(param: ParamType): ReturnType {
    // body may use: value is T, value as T, T::class, T::class.java
}

// called with an explicit type argument at the call site:
functionName<SomeType>(argument)
Element Meaning
inline Required. The function body is copied into each call site instead of compiled as one shared method.
reified Modifier on a type parameter; legal only inside an inline function. Keeps the real type available in that inlined code.
T::class The KClass of the type argument used at this call site.
T::class.java The java.lang.Class, useful when calling Java APIs that expect Class<T>.
value is T / value as T Runtime type check or cast against the reified type — illegal for a non-reified type parameter.
funcName<Type>(...) Callers pass the type argument explicitly (or let it be inferred) so the compiler knows what to substitute.

You can mix reified and non-reified type parameters on the same inline function, e.g. inline fun <T, reified R> combine(a: T, b: Any): R — only the parameters marked reified get the special treatment.

Examples

Example 1: A basic runtime type check

Without reified, a generic function has no way to answer "is this value a T?". With it, the check is just an ordinary is expression:

inline fun <reified T> isInstanceOf(value: Any): Boolean {
    return value is T
}

fun main() {
    println(isInstanceOf<String>("hello"))
    println(isInstanceOf<Int>("hello"))
    println(isInstanceOf<String>(42))
}

Output:

true
false
false

Each call substitutes a different concrete type at compile time: the first call becomes, in effect, "hello" is String; the second becomes "hello" is Int. Three calls to the same source function produce three different inlined checks.

Example 2: Reading the type’s class

T::class only works when T is reified, since it needs the actual KClass object, not an erased placeholder:

inline fun <reified T> describe(value: Any): String {
    return "Value is ${T::class.simpleName}, actual type match: ${value is T}"
}

fun main() {
    println(describe<String>("hello"))
    println(describe<Int>("hello"))
}

Output:

Value is String, actual type match: true
Value is Int, actual type match: false

T::class.simpleName reads back "String" or "Int" because, again, T has already been replaced by a concrete type by the time this code is compiled into each call site.

Example 3: A realistic filter over mixed data

A common real use of reified generics is pulling all elements of a particular type out of a heterogeneous collection — the same idea behind the standard library’s filterIsInstance<T>():

data class Employee(val name: String, val salary: Double)
data class Contractor(val name: String, val rate: Double)

inline fun <reified T> extractType(items: List<Any>): List<T> {
    val result = mutableListOf<T>()
    for (item in items) {
        if (item is T) {
            result.add(item)
        }
    }
    return result
}

fun main() {
    val workers: List<Any> = listOf(
        Employee("Alice", 85000.0),
        Contractor("Bob", 75.0),
        Employee("Carol", 92000.0)
    )

    val employees = extractType<Employee>(workers)
    println(employees)
    println("Count: ${employees.size}")
}

Output:

[Employee(name=Alice, salary=85000.0), Employee(name=Carol, salary=92000.0)]
Count: 2

Employee and Contractor are data classes, so their auto-generated toString() is what you see printed. Inside the loop, item is T both performs the check and smart-casts item to T for the following line, which is exactly why result.add(item) type-checks even though items was declared as List<Any>.

How it works step by step

Take the call isInstanceOf<String>("hello") from Example 1:

  1. The compiler sees that isInstanceOf is inline, so it does not emit a normal, callable method for it at all.
  2. At this particular call site, the type argument String is known (either written explicitly or inferred from the argument).
  3. Because T is reified, the compiler textually substitutes String for T throughout the function body as it copies that body into the call site.
  4. The bytecode generated at this call site is therefore equivalent to hand-writing "hello" is String — a completely ordinary, erasure-proof instance check.
  5. The next call, isInstanceOf<Int>("hello"), repeats the process independently with Int substituted in, producing different bytecode at that call site.

Contrast this with what would happen if isInstanceOf were an ordinary (non-inline, non-reified) generic function: there would be exactly one compiled method body, shared by every caller, in which T has been erased to Any. There is no single, correct meaning for is T in that shared body — which is precisely why the compiler rejects it outright rather than silently compiling something that could misbehave.

Common Mistakes

Mistake 1: Marking a type parameter reified without inline

reified only means anything on an inlined call site. Without inline, there is no substitution step, so the compiler rejects it immediately:

fun <reified T> broken(value: Any): Boolean {
    return value is T
}

This fails to compile: reified type parameters are only permitted on functions declared inline. The fix is simply adding the missing modifier:

inline fun <reified T> fixed(value: Any): Boolean {
    return value is T
}

fun main() {
    println(fixed<String>("hello"))
    println(fixed<Double>(3.14))
}

Output:

true
true

Mistake 2: Forwarding an ordinary type parameter into a reified call

A very common trap: you have a normal (non-inline) generic function and try to call a reified function using its type parameter. This fails because the outer function’s T has already been erased by the time it would need to be handed to the reified call:

inline fun <reified T> isInstanceOf(value: Any): Boolean = value is T

fun <T> wrapper(value: Any): Boolean {
    return isInstanceOf<T>(value)
}

This fails to compile: the compiler cannot use wrapper‘s T as a reified type argument, because wrapper itself is an ordinary generic function whose T is erased — there is nothing concrete to substitute. The fix is to make wrapper itself inline with a reified type parameter, so its own T is available to forward:

inline fun <reified T> isInstanceOf(value: Any): Boolean = value is T

inline fun <reified T> wrapper(value: Any): Boolean {
    return isInstanceOf<T>(value)
}

fun main() {
    println(wrapper<String>("hello"))
    println(wrapper<Int>("hello"))
}

Output:

true
false

Best Practices

  • Only reach for inline + reified when you actually need is T, T::class, or as T inside the function — don’t inline a large function just to get access to reified.
  • Keep reified functions small and focused (type checks, filtering, class lookups, simple parsing helpers); inlining copies the entire body into every call site, so a big function bloats the compiled bytecode of every caller.
  • If you need to forward a reified type parameter from one inline function to another, the outer function must itself be declared inline fun <reified T> — you cannot bridge from an ordinary generic function.
  • Use noinline on any function-typed parameter you don’t want copied at every call site (e.g. a large lambda you’re storing for later), while still keeping other parameters reified.
  • Remember that reified is a purely compile-time, per-call-site trick — it does not change how type erasure works anywhere else in your program, including in non-inline functions.
  • Treat the body of a public inline function as part of your library’s binary API surface, since it is compiled directly into every caller’s code, not hidden behind a method call.

Practice Exercises

  • Write inline fun <reified T> countInstances(items: List<Any>): Int that returns how many elements of items are instances of T. Test it against a mixed list containing both String and Int values and print the count for each type.
  • Write a non-inline generic function fun <T> logAndCheck(value: Any): Boolean that tries to call a reified isInstanceOf<T>(value) inside it. Observe the compile error, then fix it by making logAndCheck itself inline fun <reified T>.
  • Write inline fun <reified T> firstOrNull(items: List<Any>): T? that returns the first element of items that is an instance of T, or null if none match. Test it on a list containing a mix of Employee-like data classes.

Summary

  • The JVM erases generic type parameters at runtime, so ordinary generic functions cannot use is T, T::class, or create an Array<T> directly.
  • Marking a function inline copies its body into every call site; adding reified to a type parameter lets the compiler substitute the real, concrete type at each of those sites.
  • Reified type parameters support is, as, T::class, T::class.java, and typed array creation — none of which work on ordinary type parameters.
  • reified is only legal on inline functions, and you cannot forward a plain generic function’s type parameter into a reified call without making that function inline and reified too.
  • Use reified generics for small, reusable utilities — type filtering, checking, and class-based lookups — not for large function bodies, since inlining duplicates bytecode at every call site.