skip to content

How does Kotlin represent generic types at runtime via KType, and how do typeOf<T>() and reified parameters overcome JVM type erasure?

level: seniorimportance: should knowfreq 30%

answer

  1. JVM erases generics; List<String> -> List at runtime
  2. KType = classifier + arguments(variance) + nullability
  3. typeOf<T>() captures full static type
  4. reified needs inline; enables T::class, is T, as T
  5. Powers decodeFromString<T> / fromJson<T>

basics

~10 s

The JVM erases generics, so List<String> looks like List at runtime. Kotlin's typeOf<T>() captures the full type, and reified type parameters in inline functions keep T available so you can inspect or use it.

solid answer

~40 s

On the JVM, generic type arguments are **erased**: `List<String>` and `List<Int>` share one runtime class. Kotlin recovers full type info with **`KType`**, which carries the `classifier` (a `KClassifier`, usually a `KClass`), the list of `arguments` (`KTypeProjection`, including variance), and `isMarkedNullable`. You obtain a `KType` from `typeOf<T>()` (a stdlib intrinsic) or from members like `KCallable.returnType` / `KProperty.returnType`. **`reified`** type parameters work only on `inline fun`: the compiler substitutes the actual type at each call site, so inside the function you can write `T::class`, `is T`, `as T`, or `typeOf<T>()`. This is why `inline fun <reified T> Gson.fromJson(...)` and `Json.decodeFromString<T>()` can resolve the target type. Without inline+reified you cannot do `is T` or `T::class`. `serializer(typeOf<T>())` bridges a runtime KType to a serializer.

code

kotlin · 13 lines
kotlin
import kotlin.reflect.typeOf
import kotlin.reflect.KType

inline fun <reified T> typeInfo(): KType = typeOf<T>()

fun main() {
    val k = typeInfo<Map<String, List<Int?>>>()
    println(k.isMarkedNullable)                 // false
    val firstArg = k.arguments.first().type     // String
    println(firstArg)
    val nested = k.arguments[1].type            // List<Int?>
    println(nested?.arguments?.first()?.type?.isMarkedNullable)  // true (Int?)
}

go deeper

for a junior

Knows the JVM erases generics and reified lets you use T inside inline functions.

for a middle

Explains typeOf<T>() and the inline+reified requirement with is T / T::class.

for a senior

Describes KType structure (classifier, arguments, variance, nullability) and how it powers serialization/JSON libraries.

for a principal

Reasons about codegen/bytecode-size tradeoffs of reified inlining and designs APIs that bridge erased generics to KType-driven runtime behavior.

## JVM type erasure The JVM implements generics by **erasure**: type arguments are removed after compilation. At runtime `List<String>` is just `List`, and `x is List<String>` is impossible to check fully — only `x is List<*>` is allowed. ## KType — Kotlin's runtime type representation `KType` represents a *use* of a type with its arguments and nullability: - `classifier: KClassifier?` — the underlying `KClass` (or type parameter) - `arguments: List<KTypeProjection>` — the type arguments, each with variance (`in`/`out`/invariant) and a nested `KType` - `isMarkedNullable: Boolean` You get a `KType` from: - `typeOf<T>()` — a stdlib **intrinsic** the compiler fills in with the full static type - reflective members: `kProperty.returnType`, `kFunction.returnType`, `kParameter.type` ```kotlin import kotlin.reflect.typeOf val t = typeOf<Map<String, List<Int?>>>() println(t.classifier) // class kotlin.collections.Map println(t.arguments) // [String, List<Int?>] ``` ## reified type parameters A normal type parameter `T` is erased inside a function. An **`inline fun`** can mark a parameter `reified`, and the compiler **substitutes the concrete type** at every call site, so inside the body you may: - call `T::class` - use `x is T` / `x as T` - call `typeOf<T>()` ```kotlin inline fun <reified T> Any.cast(): T? = this as? T inline fun <reified T : Enum<T>> parseEnum(name: String): T = enumValues<T>().first { it.name == name } ``` This is exactly the mechanism behind `Json.decodeFromString<User>(s)` and `gson.fromJson<T>(...)`: the reified `T` (often turned into `typeOf<T>()`) supplies the otherwise-erased target type. ## Bridging to serialization ```kotlin import kotlinx.serialization.serializer val ser = serializer(typeOf<List<User>>()) // KType -> KSerializer ``` ## Limitations - `reified` requires `inline`; you cannot store a reified `T` in a class field or use it from a non-inline function. - Reified inlining can increase bytecode size at each call site. - `typeOf` captures the *static* type at the call site, not a runtime value's type.

  • Why can't you write a non-inline fun <T> isInstance(x: Any) = x is T?
    T is erased in a regular function, so the runtime has no type to check against; you need inline + reified so the compiler substitutes the concrete type.
  • What's the difference between T::class and typeOf<T>() inside a reified function?
    T::class gives only the raw KClass (no type arguments), while typeOf<T>() returns a full KType including generic arguments and nullability.

Erasure throws away the shipping label; typeOf<T>() and reified are like photographing the label before it's removed so you still know what's inside.

saying these in an interview costs you the question

  • Claiming you can do x is List<String> at runtime
  • Using reified without inline
  • Thinking T::class preserves generic arguments
  • Saying typeOf inspects a value's runtime type
  • Not connecting reified to decodeFromString<T>

context