Kotlin’s function type variance is an important concept that helps developers understand how types behave when dealing with function inputs and outputs. If you’ve ever wondered why function parameters and return types follow different variance rules, this post will clear things up..!
Understanding Variance in Kotlin
Variance determines how subtyping relationships work in a type hierarchy. There are two key types of variance in Kotlin:
- Covariance (Out-projected types): A subtype can replace a supertype when reading values.
- Contravariance (In-projected types): A supertype can replace a subtype when writing values.
Function Types and Variance
Kotlin functions can be represented as types, for example:
val func: (String) -> Int = { it.length }Here, func is a function type that takes a String as input and returns an Int. Function types in Kotlin have two parts:
- Parameter type(s) — What the function takes in.
- Return type — What the function returns.
The variance in function types applies differently to parameters and return types.
- Return types are covariant (
out). - Parameter types are contravariant (
in).
Now, let’s explore how these apply to Kotlin’s function type variance.
Covariance and Contravariance in Kotlin’s Function Types
In Kotlin, a class or interface can be covariant on one type parameter and contravariant on another. One of the classic examples of this is the Function interface. Let’s take a look at the declaration of the Function1 interface, which represents a one-parameter function:
interface Function1<in P, out R> {
operator fun invoke(p: P): R
}To make the notation more readable, Kotlin provides an alternative syntax (P) -> R to represent Function1<P, R>. In this syntax, you’ll notice that P (the parameter type) is used only in the in position and is marked with the in keyword, while R (the return type) is used only in the out position and is marked with the out keyword.
This means that the subtyping relationship for the function type is reversed for the first type argument (P) and preserved for the second type argument (R).
For example, let’s say you have a higher-order function called enumerateCats that accepts a lambda function taking a Cat parameter and returning a Number:
fun enumerateCats(f: (Cat) -> Number) { ... }Now, suppose you have a function called getIndex defined in the Animal class that returns an Int. You can pass Animal::getIndex as an argument to enumerateCats:
fun Animal.getIndex(): Int = ...
enumerateCats(Animal::getIndex) // This code is legal in Kotlin. Animal is a supertype of Cat, and Int is a subtype of NumberIn this case, the Animal::getIndex function is accepted because Animal is a supertype of Cat, and Int is a subtype of Number, the function type’s subtyping relationship allows it.

This illustration demonstrates how subtyping works for function types. The arrows indicate the subtyping relationship.
Function Type Variance in Kotlin’s Type System
As you now know, when working with function types in Kotlin, variance is explicitly defined:
interface Function1<in P, out R> {
fun invoke(param: P): R
}P(parameter) is contravariant (in)R(return type) is covariant (out)
This aligns with the natural logic of function type variance.
Conclusion
So in summary,
- Covariance (
out) applies to return types – A function returning a subtype can be assigned to a function returning a supertype. - Contravariance (
in) applies to parameter types – A function taking a supertype can be assigned to a function taking a subtype. - Kotlin enforces these rules to ensure type safety and prevent runtime errors.
Understanding Kotlin’s Function Type Variance helps write flexible, reusable, and type-safe code, especially when designing APIs, lambda functions, and higher-order functions.
