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:
- The compiler sees that
isInstanceOfisinline, so it does not emit a normal, callable method for it at all. - At this particular call site, the type argument
Stringis known (either written explicitly or inferred from the argument). - Because
Tisreified, the compiler textually substitutesStringforTthroughout the function body as it copies that body into the call site. - The bytecode generated at this call site is therefore equivalent to hand-writing
"hello" is String— a completely ordinary, erasure-proof instance check. - The next call,
isInstanceOf<Int>("hello"), repeats the process independently withIntsubstituted 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+reifiedwhen you actually needis T,T::class, oras Tinside the function — don’t inline a large function just to get access toreified. - 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
noinlineon 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
inlinefunction 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>): Intthat returns how many elements ofitemsare instances ofT. Test it against a mixed list containing bothStringandIntvalues and print the count for each type. - Write a non-inline generic function
fun <T> logAndCheck(value: Any): Booleanthat tries to call a reifiedisInstanceOf<T>(value)inside it. Observe the compile error, then fix it by makinglogAndCheckitselfinline fun <reified T>. - Write
inline fun <reified T> firstOrNull(items: List<Any>): T?that returns the first element ofitemsthat is an instance ofT, ornullif none match. Test it on a list containing a mix ofEmployee-like data classes.
Summary
- The JVM erases generic type parameters at runtime, so ordinary generic functions cannot use
is T,T::class, or create anArray<T>directly. - Marking a function
inlinecopies its body into every call site; addingreifiedto 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. reifiedis only legal oninlinefunctions, 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.
