The beauty of Kotlin type system
The Kotlin type system is amazingly designed. Many features that look like special cases are just a natural consequence of how the type system is designed. For instance, thanks to the type system, in the example below the type of
String, the type of
Int, and we can use
throw on the right side of the Elvis operator.
The typing system also gives us very convenient nullability support, smart type inference, and much more. In this chapter, we will reveal a lot of Kotlin magic. I always love talking about this in my workshops because I see the stunning beauty of how Kotlin’s type system is so well designed that all these pieces fit perfectly together and give us a great programming experience. I find this topic fascinating, but I will also try to add some useful hints that show where this knowledge can be useful in practice. I hope you will enjoy discovering it as much as I did.
What is a type?
Before we start talking about the type system, we should first explain what a type is. Do you know the answer? Think about it for a moment.
Types are commonly confused with classes, but these two terms represent totally different concepts. Take a look at the example below. You can see
User used four times. Can you tell me which usages are classes, which are types, and which are something else?
class keyword, you define a class name. A class is a template for objects that defines a set of properties and methods. When we call a constructor, we create an object. Types are used here to specify what kind of objects we expect to have in the variables1.
Why do we have types?
Let's do a thought experiment for a moment. Kotlin is a statically typed language, so all variables and functions must be typed. If we do not specify their types explicitly, they will be inferred. But let's take a step back and imagine that you are a language designer who is deciding what Kotlin should look like. It is possible to drop all these requirements and eliminate all types completely. The compiler does not really need them2. It has classes that define how objects should be created, and it has objects that are used during execution. What do we lose if we get rid of types? Mostly safety and developers' convenience.
So why do we have types? They are mainly for us, developers. A type tells us what methods or properties we can use on an object. A type tells us what kind of value can be used as an argument. Types prevent the use of incorrect objects, methods, or properties. They give us safety, and suggestions are provided by the IDE. The compiler also benefits from types as they are used to better optimize our code or to decide which function should be chosen when its name is overloaded. Still, it is developers who are the most important beneficent of types.
So what is a type? It can be considered as a set of things we can do with an object. Typically, it is a set of methods and properties.
The relation between classes and types
We say that classes generate types. Think of the class
User. It generates two types. Can you name them both? One is
User, but the second is not
Any is already in the type hierarchy). The second new type generated by the class
User?. Yes, the nullable variant is a separate type.
There are classes that generate many more types: generic classes. The
Box<T> class theoretically generates an infinite number of types.
Class vs type in practice
This discussion might sound very theoretical, but it already has some practical implications. Note that classes cannot be nullable, but types can. Consider the initial example, where I asked you to point out where
User is a type. Only in positions that represent types can you use
User? instead of
Member functions are defined on classes, so their receiver cannot be nullable or have type arguments4. Extension functions are defined on types, so they can be nullable or defined on a concrete generic type. Consider the
sum function,, which is an extension of
Iterable<Int>, or the
isNullOrBlank function, which is an extension of
The relationship between types
Let's say that we have a class
Dog and its superclass
Animal type is expected, you can use a
Dog, but not the other way around.
Why? Because there is a concrete relationship between these types:
Dog is a subtype of
Animal. By rule, when A is a subtype of B, we can use A where B is expected. We might also say that
Animal is a supertype of
Dog, and a subtype can be used where a supertype is expected.
There is also a relationship between nullable and non-nullable types. A non-nullable can be used wherever a nullable is expected.
This is because the non-nullable variant of each type is a subtype of the nullable variant.
The superclass of all the classes in Kotlin is
Any, which is similar to
Object in Java. The supertype of all the types is not
Any, it is
Any is a supertype of all non-nullable types. We also have something that is not present in Java and most other mainstream languages: the subtype of all the types, which is called
Nothing. We will talk about it soon.
Any is only a supertype of non-nullable types. So, wherever
Any is expected, nullable types will not be accepted. This fact is also used to set a type parameter’s upper boundary to accept only non-nullable types5.
Unit does not have any special place in the type hierarchy. It is just an object declaration that is used when a function does not specify a result type.
Let's talk about a concept that has a very special place in the typing hierarchy: let's talk about
The subtype of all the types: Nothing
Nothing is a subtype of all the types in Kotlin. If we had an instance of this type, it could be used instead of everything else (like a Joker in the card game Rummy). It’s no wonder that such an instance does not exist.
Nothing is an empty type (also known as a bottom type, zero type, uninhabited type, or never type), which means it has no values. It is literally impossible to make an instance of type
Nothing, but this type is still really useful. I will tell you more: some functions declare
Nothing as their result type. You've likely used such functions many times already. What functions are those? They declare
Nothing as a result type, but they cannot return it because this type has no instances. But what can these functions do? Three things: they either need to run forever, end the program, or throw an exception. In all cases, they never return, so the
Nothing type is not only valid but also really useful.
I have never found a good use case for a function that runs forever, and ending a program is not very common, but we often use functions that throw exceptions. Who hasn't ever used
TODO()? This function throws a
NotImplementedError exception. There is also the
error function from the standard library, which throws an
TODO is used as a placeholder in a place where we plan to implement some code.
error is used to signal an illegal situation:
This result type is significant. Let’s say that you have an if-condition that returns either
Nothing. What should the inferred type be? The closest supertype of both
Int. This is why the inferred type will be
The same rule applies when we use the Elvis operator, a when-expression, etc. In the example below, the type of both
String because both
Nothing as their result type. This is a huge convenience.
The result type from return and throw
I will start this subchapter with something strange: did you know that you can place
throw on the right side of a variable assignment?
This doesn’t make any sense as both
throw end the function, so we will never assign anything to such variables (like
b in the example above). This assignment is an unreachable piece of code. In Kotlin, it just causes a warning.
The code above is correct from the language point of view because both
throw are expressions, which means they declare a result type. This type is
This explains why we can place
throw on the right side of the Elvis operator or in a when-expression.
Nothing as their result type. As a consequence, Kotlin will infer
String as the type of both
String is the closest supertype of both
So, now you can say that you know
Nothing. Just like John Snow.
When is some code not reachable?
When an element declares
Nothing as a return type, it means that everything after its call is not reachable. This is reasonable: there are no instances of
Nothing, so it cannot be returned. This means a statement that declares
Nothing as its result type will never complete in a normal way, so the next statements are not reachable. This is why everything after either
throw will be unreachable.
It’s the same with
error, etc. If a non-optional expression declares
Nothing as its result type, everything after that is unreachable. This is a simple rule, but it’s useful for the compiler. It’s also useful for us since it gives us more possibilities. Thanks to this rule, we can use
TODO() in a function instead of returning a value. Anything that declares
Nothing as a result type ends the function (or runs forever), so this function will not end without returning or throwing first.
I would like to end this topic with a more advanced example that comes from the Kotlin Coroutines library. There is a
MutableStateFlow class, which represents a mutable value whose state changes can be observed using the
collect method. The thing is that
collect suspends the current coroutine until whatever it observes is closed, but a StateFlow cannot be closed. This is why this
collect function declares
Nothing as its result type.
That is very useful for developers who are not aware of how
collect works. Thanks to the result type, IntelliJ informs them that the code they place after
collect is unreachable.
collectfunction will never return, therefore it declares
Nothingas its result type.
The type of null
Let's see another peculiar thing. Did you know that you can assign
null to a variable without setting an explicit type? What’s more, such a variable can be used wherever
null is accepted.
This means that
null has its type, which is a subtype of all nullable types. Take a look at the type hierarchy and guess what type this is.
I hope you guessed that the type of
Nothing?. Now think about the inferred type of
b in the example below.
In the if-expression, we search for the closest supertype of the types from both branches. The closest supertype of
String?. The same is true about the when-expression: the closest supertype of
String?. Everything makes sense.
For the same reason, whenever we require
String?, we can pass either
null, whose type is
Nothing?. This is clear when you take a look at the type hierarchy.
Nothing? are the only non-empty subtypes of
In this chapter, we've learned the following:
- A class is a template for creating objects. A type defines expectations and functionalities.
- Every class generates a nullable and a non-nullable type.
- A nullable type is a supertype of the non-nullable variant of this type.
- The supertype of all types is
- The supertype of non-nullable types is
- The subtype of all types is
- When a function declares
Nothingas a return type, this means that it will throw an error or run infinitely.
Nothingas their result type.
- The Kotlin compiler understands that when an expression declares
Nothingas a result type, everything after that is unreachable.
- The type of
Nothing?, which is the subtype of all nullable types.
In the next chapter, we are going to discuss generics, and we’ll see how they are important for our type system.
Parameters are also variables.
Except when figuring out which function to choose in the case of overloading.
Type arguments and type parameters will be better explained in the chapter Generics.
I will explain type parameters' upper boundaries in the chapter Generics.