
The Kotlin compiler and type system are our best friends when it comes to writing reliable and secure code, so we should always try to make best use of the provided language features.
In this blog post, we’ll have a look at:
- Kotlin’s type hierarchy and special types (
T?,Any,Unit,Nothing) - generic types, upper bounds, and type erasure
- covariant, contravariant, and invariant generic types
- projected types (
in,out,*)
There’s a TL;DR after each section but I’ll provide brief explanations and motivational examples as well. 😎
See also the Kotlin Language Guide on parts of the content presented here. You also might be interested in some older blog posts:
Table of Contents
Kotlin’s Type Hierarchy
Let us first take a quick look at Kotlin’s most important types and their precise meaning.
Nullable Types
As you probably know, Kotlin strictly distinguishes nullable and non-nullable types. Every type T has a nullable counterpart T?. The objects of T are never null, and the objects of T? are precisely those of T and null. This is a core design principle of Kotlin.
Small Remark – Note that null isn’t a type itself, it’s a value that belongs to every nullable type (Int?, String?, …). The return type of a nonsensical function that only returns null would be Nothing?, which is a type that contains only the object null (see below).
Any
The Any type is the most general non-nullable type that Kotlin has to offer. Every non-null object is of type Any, so every non-nullable type is a sub-type of Any.
It follows that Any? is truly the most general type in Kotlin, since it also includes null. Every object is of type Any?, and every type is a sub-type of Any?.
Unit
The Unit type is a type with a special meaning. It is a type with only one object (hence the name “Unit”) which does not carry any interesting information.
We usually want functions to return Unit when they only perform side effects. The Unit return type then means: That function – in fact – did something and returned, but no object or value of interest came out of it.
So to speak, Unit signalizes that a function returns, not what it returns.
Nothing
The Nothing type is another special type. It has no objects.
That means that any object you can think of (any Int, String, …) can never be of type Nothing.
It trivially follows that Nothing is a sub-type of every other type (since there are no objects of Nothing that could violate this condition).
In particular, if a function () -> Int for some reason returns Nothing, the result is still an Integer (since Nothing is a sub-type of Int).
This is – for instance – the case if the function throws. And a function that always throws (for instance, the TODO function) will always just return Nothing, as it doesn’t return.
If you’re familiar with Arrow, you’ll also know that every None is an Option<Nothing>, every Left<A> is an Either<A, Nothing>, etc.
An Illustration
I have drawn a neat little directed graph to illustrate this. An arrow means “is sub-type of” (I didn’t draw all possible arrows of the transitive closure though).

The key observation: Nothing is a lower bound of every type, and Any? is an upper bound of every type, i.e. they are the “minimum” and “maximum” types in Kotlin’s type system. No object is Nothing and every object is Any?.
TL;DR
T?is the nullable counterpart of the non-nullable typeT, i.e.T?is preciselyTandnullAny?is the largest type, containing every objectUnitcontains exactly one objectNothingis the smallest type, containing no objects
Generic Types
Kotlin allows classes, interfaces, and functions to have type parameters. A type with a type parameter is called a generic type.
A universal example would be the type List<T> – a list of that type is guaranteed to only contain elements of type T. Or the type Pair<A, B>, whose objects are tuples with the first entry of type A and the second entry of type B.
We can then define generic functions like this:
Upper Bounds
If we want to restrict the type parameter of a generic class, interface, or function to be a sub-type of another type, we can do it like this:
Then Spawner<Cat> spawns cats, and Spawner<Animal> spawns any kind of animal, …

Multiple Upper Bounds
Sometimes we want to enforce multiple upper bounds for a type parameter that should hold at the same time. We can use Kotlin’s where keyword for this.
Then T is assumed to be both Animal and Vehicle at the same time. This way, we can define extension functions on type intersections.

Type Erasure
Note that generic types are a compile-time feature only! This doesn’t work:
The reason is that the type parameter T gets erased at runtime. In order to make this code work, we have to inline isOfType and reify T.
Then, we can happily define and call cat.isCat().
TL;DR
- Kotlin supports classes, interfaces, and functions with type parameters
- you can declare one or (using the
wherekeyword) more upper bounds for type parameters - type parameters are erased at runtime unless reified
Variance
Assume we have two types with a sub-type relationship (say, Cat and Animal).
The term “variance” means whether and how the sub-type relationship Cat ⊂ Animal is carried over to generic types SomeType<Cat> and SomeType<Animal>.
Covariance
Let’s look at the Spawner code from the previous section again.
The technical model is clear: Every cat is a special case of an animal, so every cat spawner is a special case of an animal spawner.
That means we want Spawner<Cat> to be a sub-type of Spawner<Animal> on the code level as well.
The compiler doesn’t know this automatically (and it shouldn’t, as this implication might not hold for other technical models), but we can let it know of this fact by adding the out keyword before the type parameter.
We say that Spawner<T> is covariant in T – it preserves the sub-type relation.
Note that the out declaration can only be applied if the interface only defines methods that have T as output. We are not allowed to do this if T somewhere appears as a function argument – see below why.

Contravariance
Let us now define a PokeBall interface that catches a specific kind of animal, like this.
Note that T is now being consumed instead of being produced.
The technical model is now like this: A PokeBall<Cat> only captures cats, and a PokeBall<Animal> captures any kind of animal – in particular, cats! It follows that every animal Pokéball is automatically a cat Pokéball.
That means we now want PokeBall<Animal> to be a sub-type of PokeBall<Cat>, which is the other way around as in the Spawner example.
Again, we can let the compiler know this by now adding the in keyword before the type parameter:
We say that PokeBall<T> is contravariant in T – it reverses the sub-type relation.
Again, the in declaration can only be applied if the interface only defines methods that have T as input. We are not allowed to do this if T somewhere appears as a function result.

Invariance
It is obvious that any meaningful generic type cannot be covariant and contravariant in its type parameter at the same time. Look at the following example:
We can neither declare T as out nor as in. The compiler prevents us, and the implications wouldn’t make any sense: The statements Dealer<Cat> ⊂ Dealer<Animal> and Dealer<Animal> ⊂ Dealer<Cat> cannot be true the same time.
We call such a type invariant in its type argument.
Invariant types are a little less flexible than covariant or contravariant types. Assume we want to browse the inventory of an animal dealer:
This definition would be problematic: browseAnimals cannot have a cat dealer as its argument because Dealer<Cat> is not a sub-type of Dealer<Animal>.
Similarly, assume we want to sell all our hamsters to a hamster dealer:
Then this would be problematic as well: We cannot pass an animal dealer to this function because Dealer<Animal> is not a sub-type of Dealer<Hamster>.
One way to resolve this would be splitting up the Dealer interface in one covariant and one contravariant type like this:
But this doesn’t seem convenient in every situation… I’ll show you another way using type projections in the next section.
TL;DR
- add the
outkeyword to make a generic type covariant (preserve the sub-type relation) – only possible if there are only producer methods - add the
inkeyword to make a generic type contravariant (reverse the sub-type relation) – only possible if there are only consumer methods - if neither is possible, the type is invariant (and one has to be careful with signatures)
Projected Types
With only a small tweak, we can make the browseAnimals and supplyWithHamsters functions behave as expected.
The “out” Projection
We can add the out keyword in the function definition as follows:
Here, Dealer<out Animal> is the “covariant part” of the Dealer<Animal> type, i.e. we only have access to the “out” methods – in this case, sell. We cannot access buy because buy breaks covariance! So to speak, the projected type is what could have been defined as Seller<Animal>.
With this little tweak, we can happily call browseAnimals(catDealer)!
The “in” Projection
Similarly, we can add the in keyword like this:
Of course, the type Dealer<in Hamster> is then the “contravariant part” of Dealer<Hamster>, and we can access all “in” methods – in this case, buy. But again, we cannot access sell because sell breaks contravariance. The projected type is what could have been defined as Buyer<Hamster>.
Now, calling supplyWithHamsters(animalDealer) works just fine.
Declaration-site Variance vs. Use-site Variance
If we define types like Spawner<out T : Animal>, the out-declaration happens at the class or interface definition level, so it’s known to the compiler everywhere. This kind of variance is called declaration-site variance.
On the other hand, if we use out and in in function arguments, only the function knows of the variance, which is why this is called use-site variance.
Note that while out and in function arguments are a convenient little tweak, we lose type information.
- The result of
browseAnimals(catDealer)will be typed asList<Animal>, losing the information that the dealer specifically sells cats. - And a
Dealer<Animal>supplied with hamsters will become aDealer<Hamster>, losing the information that the dealer possibly sells other animals than hamsters.
So we should use it only when we don’t care about the specific types. (Otherwise, it would be better to just write a generic function instead.)
The Star Projection
Lastly, have a look at the following code snippet.
What is the type of dealers?
We might expect it to be Dealer<Animal>… But this cannot be the case because neither Dealer<Cat> nor Dealer<Dog> are sub-types of Dealer<Animal> (as the type is invariant). In fact, the smallest common super-type of catDealer and dogDealer is just Any.
But wait! We know that the objects of dealers are in fact of type Dealer<...> – how to resolve this?
For this purpose, Kotlin provides the star projection. We can write something like this:
The projected type Dealer<*> means: It’s a dealer that deals with animals of some kind – but we don’t know which kind and we don’t care as well. We’re totally fine if the result is just List<Animal>, living a life in blissful ignorance.
Note that putting List<Dealer<Animal>> there doesn’t work nicely, as then we wouldn’t be able to pass the previously defined list dealers as an argument to this function. Also note that we cannot access buy because, again, it breaks variance (we might not be able to sell dogs to a cat dealer).
If there isn’t a specific common upper bound of the type parameters, then SomeType<*> just resolves to SomeType<out Any?>.
TL;DR
- the
outandinkeywords can be applied to generic types in function arguments as well (use-site variance as opposed to declaration-site variance) - we then get projections of our generic type onto its “covariant part” or “contravariant part” only within that function (the trade-off being type information)
- we can use the star projection to “group” multiple objects of the same generic type if we don’t care about the type parameter
Summary
You should now be able to understand:
- Kotlin’s special types
T?,Unit,Any, andNothing - how to define generic types with one or more upper bounds on their type parameters
- how to use the
outandinkeywords to make your types declaration-site variant - how to use the
outandinkeywords to make your types use-site variant via projection - the difference between
SomeType<*>(star projection) andSomeType<Any?>
I hope that these explanations help you writing safer and more expressive Kotlin code. 🙂

You must be logged in to post a comment.