Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Eliminating Unsafe Casts with Reified Type Parameters

Hello! Last lesson, you used variance to make promises about how a generic API handles values. This lesson addresses a different limit of generics: what happens when a function needs to identify a type while the app is running.

You’ll see why a generic cast can compile yet still be unsafe, then replace it with an inline function whose reified type parameter supports a checked type test. This is useful when an Android-facing API hands you loosely typed values, though it is not a reason to make your own app state loosely typed.


Why an ordinary generic function cannot check its type

Suppose a callback gives you values typed as Any?, and you want the first value of a requested type. An ordinary generic function cannot write this test:

// Does not compile:
fun <T : Any> firstOfTypeOrNull(values: Iterable<*>): T? {
    for (value in values) {
        if (value is T) return value
    }
    return null
}

On the JVM, an ordinary generic function does not receive a runtime representation of its type argument T. Its callers may request String or Int, but the function body has no usable T to test against. This is type erasure.

The IDE rejects `T::class.java` in an ordinary generic function: `T` has no runtime class available there. Its suggested fix adds both `inline` and `reified`.

Watch Dave Leeds’s Reified Type Parameters for a view of the compiled-code problem and the call-site solution.

Reified Type Parameters

Dave Leeds’s “Reified Type Parameters” shows why an ordinary generic function loses its type argument and why inlining changes what the compiler can generate.

Watch type erasure for the single compiled generic function, failed type check for the resulting restriction, and inline fix for the reified solution. Then skip to trade-offs. Focus on what becomes known at the call site; you do not need the intervening XML example for this lesson.

The Kotlin documentation makes an important distinction: generic arguments are checked when you write a call, but an object such as a list does not generally retain its element type for a runtime check. Read the two short subsections below, especially their final qualification about reified checks.

Generics: in, out, where | Kotlin Documentation

Read the Kotlin Documentation’s explanation of erasure to distinguish a checked call to a generic function from a runtime check inside that function.

In “Type erasure,” start with the paragraph explaining what generic instances retain at runtime. Continue through “Generics type checks and casts,” ending with its qualification about generic instances. Read erasure and limits. Pay particular attention to why a cast to an ordinary type parameter is unchecked.

One tempting workaround is as? T:

fun <T> firstClaimedValue(values: List<Any?>): T? =
    values.firstOrNull() as? T // Warning: unchecked cast

The ? makes a checkable failed cast return null; it does not restore the erased identity of T. A call requesting String might receive an Int from this function, with a ClassCastException when the caller uses the claimed result. Suppressing the warning would hide the missing guarantee, not supply it.


Put the type test at the call site

Kotlin permits a type parameter to be marked reified only on an inline function. Inlining lets the compiler generate the function’s operation at each call site, where the requested type is known:

inline fun <reified T : Any> firstOfTypeOrNull(
    values: Iterable<*>
): T? {
    for (value in values) {
        if (value is T) return value
    }
    return null
}

For an Android feature that receives mixed callback values, you can now ask for a particular kind of value without asserting that the first value must have that type:

val callbackValues: List<Any?> = listOf(404, "Offline", null)

val message: String? = firstOfTypeOrNull<String>(callbackValues)
// message is "Offline"

val retryCount: Int? = firstOfTypeOrNull<Int>(callbackValues)
// retryCount is 404

value is T performs the type test. Inside that branch, Kotlin knows value can be returned as T; there is no unchecked as T for you to write. If nothing matches, the function returns null. The bound T : Any keeps this example about non-null types: null is not treated as a match, and null also communicates “no match.”

This is the point to read Kotlin’s Inline functions documentation. It explains both modifiers, rather than treating reified as a way to switch off erasure everywhere.

Inline functions | Kotlin Documentation

The Kotlin Documentation explains what gets inlined and then shows a reified function replacing a class-parameter-and-cast design.

In the opening “Inline functions” section, read inline basics, then note the following paragraph’s code-size caution. In “Reified type parameters,” read the reified example. Compare its original explicit class argument and cast with the version using is T.

Reified does not make the underlying values trustworthy. It lets this function test the runtime type of each candidate. If your own search-screen model is already a sealed SearchUiState, keep that stronger model; do not convert it to Any? just to use a generic helper. The helper earns its place at a boundary that is genuinely loosely typed.


When a library asks for a class instead

Some JVM and Android APIs ask for an explicit Class<T>. You can keep a class-taking function as the implementation and offer Kotlin callers a small reified convenience wrapper:

fun <T : Any> findWithClass(
    values: Iterable<*>,
    type: Class<T>
): T? {
    for (value in values) {
        if (type.isInstance(value)) return type.cast(value)
    }
    return null
}

inline fun <reified T : Any> findWithClass(
    values: Iterable<*>
): T? = findWithClass(values, T::class.java)

val message = findWithClass<String>(callbackValues)

Here T::class.java supplies the class argument that the non-inline implementation needs. isInstance checks a candidate before Class.cast returns it. The wrapper removes repeated class arguments from Kotlin call sites; it does not require the class-taking implementation to be inline. Keeping that implementation is also useful if Java callers need the API, because they cannot call the reified wrapper in the same way Kotlin callers can.

Choose between the two patterns according to the boundary. For a small Kotlin-only filter, value is T is direct. If an existing API requires Class<T>, a compact reified wrapper can improve its Kotlin call sites without obscuring the explicit class-based contract.


Know what the check cannot prove

Consider this call:

val unknown: List<Any?> = listOf(listOf(1, 2))
val claimedStrings: List<String>? =
    firstOfTypeOrNull<List<String>>(unknown)

claimedStrings may be non-null. The check can recognize a List at runtime, but it cannot verify that the list contains String elements: those element-type arguments are still erased. Reading an element as a String can therefore fail. Reification makes checks against a reified type parameter available; it does not make every nested generic argument runtime-checkable.

If element types matter, validate the elements and build a safely typed result rather than accepting the outer-list check as proof. Similarly, a reified type argument cannot be forwarded from an ordinary, erased type parameter as though the caller’s concrete type were known.

Finally, inlining copies generated code to call sites, so keep reified helpers focused rather than placing a large workflow inside one. For a public library API, changing an inline function can also affect callers that were compiled against its earlier body. These are design trade-offs, not reasons to avoid the feature when a runtime type check is genuinely needed.


Key takeaways

An ordinary generic function cannot reliably test T at runtime, and as? T is not a safe substitute. A compact inline function with reified T can use is T or obtain T::class.java, removing an unchecked cast or a repetitive class argument. Check the limit before relying on the result: reification does not verify erased type arguments inside a value such as List<String>.

Next lesson, you’ll turn to equality and mutation-related collection bugs—another place where code that looks type-safe can still produce surprising behavior.

Can't find a good explanation? Sign up and we'll make it for you

Sign up