skip to content

How does Kotlin infer the type argument of a generic function at a call site, and when does inference fail?

level: middleimportance: must knowfreq 70%

answer

  1. Two constraint sources: arguments + expected/target type
  2. Mixed args → least upper bound (listOf(1,"a") = List<Any>)
  3. T only in return type + no target → must specify
  4. Lambdas contribute their return type
  5. Failure message: 'cannot infer type' → add <...>

basics

~20 s

Kotlin guesses T from the values you pass in or from how the result is used, so you usually don't write the type yourself. It fails when there's nothing for it to read the type from.

solid answer

~50 s

At a call site Kotlin performs type-argument inference: it derives each type parameter from the types of the supplied arguments and, if needed, from the expected type of the surrounding context (the return-type target). For fun <T> first(list: List<T>): T, calling first(listOf(1, 2)) infers T = Int from the argument. When the parameter doesn't appear in any value argument — e.g. fun <T> empty(): List<T> = emptyList() — there is nothing to infer from, so you must supply it explicitly (empty<String>()) or let the assignment target drive it (val xs: List<String> = empty()). Inference also picks the least upper bound across mixed arguments: listOf(1, "a") infers List<Any>. It runs alongside overload resolution and can use lambda return types. When ambiguous or unconstrained, the compiler reports it can't infer the type and asks for explicit type arguments.

code

kotlin · 9 lines
kotlin
fun <T> firstOf(list: List<T>): T = list[0]
val a = firstOf(listOf(1, 2))          // T = Int (from argument)

fun <T> empty(): List<T> = emptyList()
val b: List<String> = empty()          // T = String (from target type)
// val c = empty()                     // ERROR: cannot infer T
val c = empty<String>()                // explicit type argument

val mixed = listOf(1, "a")             // List<Any> via least upper bound

go deeper

for a junior

Knows that you usually omit the type because the compiler reads it from the arguments.

for a middle

Explains both constraint sources (arguments and target type), the LUB rule, and recognizes the 'cannot infer' failure and its fix.

for a senior

Reasons about inference with lambdas, return-only parameters, and when to add explicit arguments for readability or to break ambiguity.

for a principal

Understands inference interacting with overload resolution and Kotlin 2.x postponed/builder inference, and designs signatures so callers rarely need explicit type arguments.

## Type-argument inference **Inference** is the compiler computing the value of each type parameter so you don't have to write `foo<...>` explicitly. For a generic function the compiler collects **constraints** from two sources: 1. **Value arguments** — the static types of what you pass. 2. **Expected type (return-type context)** — the type the call's result is being assigned to or returned as. ```kotlin fun <T> firstOf(list: List<T>): T = list[0] val n = firstOf(listOf(10, 20)) // T inferred = Int from the argument val s: String = firstOf(listOf("x")) // T = String ``` ## Least upper bound across arguments When multiple arguments contribute, the compiler picks the **least upper bound (LUB)** — the most specific common supertype: ```kotlin val xs = listOf(1, "a") // List<Any> — LUB of Int and String ``` ## When inference needs the expected type If `T` appears only in the **return position** (not in any value parameter), arguments give no information. Then inference relies on the **target type**: ```kotlin fun <T> empty(): List<T> = emptyList() val names: List<String> = empty() // OK: target type fixes T = String val whatever = empty() // ERROR: cannot infer T ``` To fix the error you supply explicit type arguments: `empty<String>()`. ## Lambdas and inference The compiler can infer from a lambda's return type and can flow a type parameter into a trailing lambda's parameter: ```kotlin fun <T, R> List<T>.mapEach(f: (T) -> R): List<R> = map(f) listOf(1, 2).mapEach { it.toString() } // T=Int from receiver, R=String from lambda body ``` ## Common failure modes - **Unconstrained parameter**: `T` only in the return type and no target type → "cannot infer type". - **Conflicting constraints**: arguments demand incompatible types. - **Builder/PCLA cases**: Kotlin 2.x improved *postponed* inference for builder-style lambdas, but deeply nested unconstrained generics can still need a hint. The remedy is always the same: provide **explicit type arguments** or annotate the target. Inference never weakens type safety — it only saves typing.

  • Why does val xs = listOf(1, "a") infer List<Any> rather than failing?
    Inference computes the least upper bound of the argument types; the most specific common supertype of Int and String is Any, so T = Any.
  • Give a case where you must supply the type argument explicitly.
    When the type parameter appears only in the return type with no value argument and no expected target type, e.g. val x = emptyList() with no declared type — you write emptyList<String>().

saying these in an interview costs you the question

  • Claiming Kotlin can never infer when T is only in the return type (it can, from the target type)
  • Saying inference uses runtime values rather than static types
  • Believing inference can change/loosen type safety
  • Not knowing the least-upper-bound rule for mixed arguments
  • Thinking you must always write explicit type arguments

context