The beauty of Kotlin typing system

Marcin Moskala
Kt. Academy
Published in
7 min readMar 4, 2019

--

Kotlin typing system is amazingly designed. It gives us very comfortable nullability support, type inference, universal guards, and much more. For instance:

In this article, we will reveal a lot of Kotlin magic. I always love talking about it on my workshops, because I see the stunning beauty of how this was designed so that all those pieces fit perfectly together and give us great programming experience. There are also some useful hints, how typing system can be used in different cases. Enjoy :)

General type hierarchy

Let’s say that you defined a class Customer. This class generates two types: nullable version Customer?, and non-nullable Customer. What is the relationship between them?

Well, nullable cannot be used directly. It needs to be unpacked first into non-nullable. On the other side non-nullable can always be used as a nullable:

In any case, non-nullable can be used as nullable. This is possible because there is a relation between those two types. A nullable type is a supertype of non-nullable.

There are a few more interesting facts in the Kotlin type hierarchy. Here is the whole hierarchy in a single picture:

You can notice two interesting elements:

  • There is a supertype of all types
  • There is a subtype of all types

Supertype of all types: Any?

There is a supertype of every non-nullable type named Any. Respectively its only supertype is Any? which is the supertype of every type including nullable. The concept of a superclass of all classes is known from Java, where we have Object. Any? can be used the same way to say that we are accepting anything:

println from the stdlib

Though you might also notice that if you want only non-nullable types you can use Any. This fact is widely used for generics to say that we accept only non-nullable as a type argument:

notNull delegate from the stdlib.
Example from the previous article

Subtype of all types: Nothing

In Kotlin there is a subtype of all types — Nothing. It can be treated as any type, though there is no instance that is Nothing. When I start teaching about it, it is something new for most developers. Why would we need something like that?

Let’s start from a simpler question: Why would it make sense to declare Nothing as a return type? Stop reading for a minute and think about it.

There are no instances of Nothing, so we cannot return it. Though are there alternative ways how we can escape a function? Well, there is one. We can escape function without returning anything. We can throw an exception instead. As it turns out, the declared return type of throw is Nothing. This is where we declare Nothing as a return type, to throw exceptions:

Notice results of the fact that Nothing is a subtype of all types. Let’s say that you have if-condition that either returns String or Nothing. What should be the inferred type? The closest supertype of both String and Nothing, that is String. This is why here data inferred type is String:

This function can be used in many other contexts as well. For instance, we can use it on the right side of Elvis operator:

The contract for Elvis operator is that inferred return type is a non-nullable version of the left side and the closest supertype with the right side. On the left side we have String?, and its non-nullable version is String. On the right side we have Nothing. The closes supertype of String and Nothing is String, because Nothing is subtype of all types including String.

For the same reason, we can throw an error on the right side: its declared return type is Nothing.

Similarly, we can use a throw on control structures that can be used as expressions:

Where is Unit?

I am often asked at this point where is Unit. It is nothing special from the typing system point of view. It is just an object declaration (singleton) and in the typing system it is, like all other types, subtype of Any and supertype of Nothing.

How Unit is defined

When a code is not reachable?

When an element declares Nothing as a return type, it means that everything after that is not reachable. It must be this way because there are no instances of Nothing. This is why when we use either fail or throw, everything after those expressions will be unreachable:

For this reason, we can have everything on the left side and we don’t need to use a return statement:

This fact is often used by the most popular function that returns Nothing, TODO:

I use it frequently on the tasks in my workshops when I give functions to implement:

Though throw is not the only construct that declares nothing as a return type. Can you guess what is the second one?

It is return. Return returns from a function, so everything after a return is not reachable. It escapes the function, so we might say that inside this function it returns nothing. This is consistent with the throw, and this is how the typing system knows that everything after return is not reachable.

From the same reason, we can use return in a variety of control structures:

Notice that this finally explains why we can use return on the right side of the Elvis operator:

The type of null

Imagine that you declared a variable or a list, and you set its value to null, but you forgot to declare a type. As a result, you see the following error:

Can you explain it? Spend a moment and think about it.

The reason for this error is that when we do not declare variable or property type, the type will be taken from the right side. This is what happened here, and the type turned out to be Nothing?. So you can see that the type of null is Nothing?. Why is that?

Let’s start from noticing that null is the only possible instance of Nothing?. There is no object of type Nothing, and nullable version of any type can be null. Therefore when we see Nothing?, we can be sure that only null can be there. This fact is used when we want property delegate to be used only at top-level, where thisRef is null:

Another thing to notice is that it gives us perfect type inference. Take a look again at typing hierarchy:

Now think what will be the inferred type of a and b:

In the if, we search for the closest supertype of types from both branches. Closest supertype of String and Nothing? is String?. The same is true about when: The closest supertype of String, String, and Nothing? is String?. Everything makes sense.

From the same reason whenever we require String?, we can pass either String or null, which is Nothing?. This is clear when you take a look at the type hierarchy. Those are two subtypes of String?.

Summary

At this article I showed a few facts:

  • Every class generates a nullable and non-nullable type
  • Nullable type is a supertype of the non-nullable one
  • The supertype of all types is Any?
  • The supertype of non-nullable types is Any
  • The subtype of all types Nothing
  • The subtype of all nullable types Nothing?
  • When function declares Nothing as a return type, it means that it will throw an error or fun infinitely
  • Both throw and return declare Nothing as a return type
  • When an expression declares Nothing as a return type, everything surely executed after that will be unreachable
  • The type of null is Nothing?, what gives us expected type inference and thanks to that null can be used whenever a nullable type is expected.

The picture to remember is the illustration of the Kotlin type hierarchy:

Click the 👏 to say “thanks!” and help others find this article.

To be up-to-date with great news on Kt. Academy, subscribe to the newsletter, observe Twitter and follow us on medium.

--

--

Kt. Academy creator, co-author of Android Development with Kotlin, author of open-source libraries, community activist. http://marcinmoskala.com/