Maintaining crisp code with type aliases

For us lot, who strive to clean up and make our code look pretty, Kotlin has a nifty little feature. It's called type aliasing, and we can harness the functionality with the keyword typealias.

This week, we will be seeing where and how type aliasing makes sense and how to start using type aliases in our Kotlin based projects right away.

Type aliases, like a.k.a.?

For those who are new to the concept of type aliases, here's a quick primer:

A type alias is like another name for one of our existing types to make it look and resolve differently in our code.

Typealias or also known as

Take this as an example:

To declare a list of users, we generally write List<User>. What if, we want to make this declaration look more readable and natural like Users or UserList?

Traditionally, we would declare a plain model class to mask the less attractive List<User> to something like Users. Kind of like this:

data class Users(val userList: List<User>)

With type aliases, we can have the same result with less burden and no extra classes.

When I say less burden, what I'm trying to mean is that type aliases don't add a new type to our project. Therefore, there is no additional heap allocation, as there is no class initialisation.

Also, type aliases have the exact behaviour of the underlying type. Whatever our original class can do, a type alias can as well.

The aliasing process is straightforward

For our list of users, we can declare a type alias as the one below:

typealias Users = List<User>

This little declaration allows us to use our new Users type exactly like a list. Therefore, we can call list methods like add() on Users and even use cool Kotlin features like subscripts.

Take a look at the following code snippet:

typealias Users = MutableList<User>
val users: Users
users.add(User(name = "Rahul"))

While this might not look much of an improvement apart from the initial fancy Users type, this is one of those little things which makes our code read like sentences.

Apart from this, we can level up our type aliasing game to cases such as:

Accommodating conflicting class names

Another use case where type aliases shine out is while importing two or more classes with the same name but placed in different packages.

You might have come across situations where some of your class names conflict with classes from a framework that you are using.

For Android developers, a typical example is Android's View class.

If we were to create our very own View class, we are at the complete liberty to do so, but with the tradeoff that we can't refer to both of them as View in a single class or file.

We have to refer one of them with their fully qualified name, like this:

fun compareViews(androidView: android.view.View, appView: View)

For cases similar to this, we can use type aliases to mask the framework's classes with a different name, such as:

typealias AndroidView = android.view.View

Now, instead of relying on the fully qualified class names to differentiate our classes, we can have readable names like:

fun compareViews(androidView: AndroidView, appView: View)

Doesn't it look better now?

A note of caution though

Type aliases don't create new types. I repeat type aliases are just a different name, just like you might have two names, one formal and the other, your super-cool spy name.

Why does knowing this matter?

Because type aliases don't mean type safety, they are like caskets for your types but identical to one another.

Consider this example, suppose we are building a maps function which accepts a latitude and longitude to display a map of the place:

fun showMap(latitude: Double, longitude: Double)

We can make this function more readable by adding type aliases to separate the latitude from the longitude.

typealias Latitude = Double
typealias Longitude = Double
fun showMap(latitude: Latitude, longitude: Longitude)

While this above snippet will compile and run just fine, we can still easily pass a Longitude object as latitude to the function, and the compiler won't budge.

val latitude: Latitude = 12.7
val longitude: Longitude = 88.5
showMap(longitude, latitude)

The reason behind this is although we aliased the Double type into "apparently" two new types, Kotlin didn't actually create the types Latitude and Longitude.

It just let us use Double by the names Latitude or Longitude. Therefore, the type Latitude is precisely the same as the type Longitude. They're both Double underneath.

If we open up the equivalent Java file of our Kotlin code, we will see that the none of our type aliases are present in the Java code. They were all replaced with their actual types when Kotlin compiled our code.

Also, any extension function or property we add to our type aliases will reflect in all instances of the underlying type.

For type safety, one way is to create model classes like this:

data class Latitude(value: Double)

Another way is to use Kotlin's inline classes. We will discuss more on this topic in a later article.

Common places where we can type alias

Masking lambdas in our callbacks:

typealias Result<T> = (T) -> Unit
fun fetchData(callback: Result<String>)

Marking nullable types:

typealias MaybeBook = Book?

Make nested classes look great:

typealias DialogBuilder = AlertDialog.Builder
val dialog = DialogBuilder().build()

I like type aliases because...

Although at first, they might seem to abstract away the actual types making it hard to know what's going on in the code, they are not that bad.

If we ever want to find out what the real type is, we can /Ctrl + Click on the alias to get to the declaration point. Also, we can see the underlying type in the autocomplete section while referring to the type alias name.

Peek underlying type on auto-complete

Over time, it's something we get used to and keeps our codebase comfortable to read through and understand for us and also our teammates.

For me, one of the main things I use type aliases for is masking lambdas as I showed before. With auto-suggest to help, it's easy to glide through writing lambdas using type aliases in a few keystrokes.

One more thing

We can only add type aliases at the top level in our Kotlin file. Therefore, unlike Swift, we cannot declare a type alias inside a class or an interface.

The following code won't compile and give us a compile-time error:

class OkKotlin {
    typealias SuperInt = Int

However, this one will work just fine:

typealias SuperInt = Int
class OkKotlin {
    val superInt: SuperInt

This is the only major drawback when using type aliases as we have to declare them at the top-level instead of scoping out to classes.

We are, however, free to scope module or file-level access by using the regular visibility modifiers like private and internal.

Now that you have witnessed the magic of type aliasing, will you be refactoring your code with type aliases anytime soon?

Here's a sketch note on the topic

Kotlin type alias sketch note