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.
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"))
println(users[0])
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.
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?