Author profile picture

All About Type Aliases in Kotlin

Have you ever had a conversation like this?

Comic strip - why type aliases are helpful

Hopefully you haven’t had a conversation like that in real life, but you might have had one like that with your code!

For example, take a gander at this:

interface RestaurantPatron {
    fun makeReservation(restaurant: Organization<(Currency, Coupon?) -> Sustenance>)
    fun visit(restaurant: Organization<(Currency, Coupon?) -> Sustenance>)
    fun complainAbout(restaurant: Organization<(Currency, Coupon?) -> Sustenance>)
}

When you see a chunk of code with so many types smushed together, it’s easy to get lost in the details. In fact, it’s intimidating just looking at those functions!

Thankfully, Kotlin gives us an easy way to simplify this complex type into something way more readable - type aliases.

In this article:

  • We’re going to learn all about type aliases and how they work.
  • Then, we’re going to look at some ways you might want to use them.
  • And then we’ll look at a few gotchas to watch out for.
  • And finally, we’ll take a look at a similar concept, Import As, and see how it compares.
Undercover agent cartoon

Wheels up - let’s go!

Introducing Type Aliases

Once we’ve coined a term for a concept, we don’t have to describe that concept every time we talk about it - we just use the term instead! So let’s do the same thing for our code - let’s take this complex type and give it a name.

We’ll do this by creating a type alias:

typealias Restaurant = Organization<(Currency, Coupon?) -> Sustenance>

Now, instead of describing the concept of a restaurant everywhere - that is, instead of writing out Organization<(Currency, Coupon?) -> Sustenance> each time - we can just write the term Restaurant, like this:

interface RestaurantPatron {
    fun makeReservation(restaurant: Restaurant)
    fun visit(restaurant: Restaurant)
    fun complainAbout(restaurant: Restaurant)
}

Wow! So much easier on the eyes, and there’s a lot less thinking you have to do when you look at it!

We’ve also avoided a lot of duplication of types throughout the RestaurantPatron interface - instead of writing out Organization, Currency, Coupon?, and Sustenance each time, we’ve got just one type - Restaurant.

This also means that if we needed to change that complex type in any way - for example, if we wanted to specialize it to this: Organization<(Currency, Coupon?) -> Meal> - then we can just change it in one spot instead of three:

typealias Restaurant = Organization<(Currency, Coupon?) -> Meal>

Easy!

You might be thinking…

Readability

You might be saying to yourself, “I don’t see how this helps readability… Why would I need the type to be Restaurant in the example above, when the parameter name already clearly says restaurant? Can’t we use concrete parameter names and abstract types?”

Yes, the name of the parameter does explain the the type in more concrete terms, as it should. But the aliased version of our RestaurantPatron interface above is still more readable and less intimidating.

Also, there are cases where you either don’t have names, or they’re farther removed from the type. For example:

interface RestaurantService {
    var locator: (String, ZipCode) -> List<Organization<(Currency, Coupon?) -> Sustenance>>
}

In this code, it’s still possible to tell that the locator is returning a list of restaurants, but the only clue we have about that is the name of the interface. The essence of the locator function type gets lost in the verbosity.

This version is easier to understand with just a glance:

interface RestaurantService {
    var locator: (String, ZipCode) -> List<Restaurant>
}

Indirection

You might also be thinking, “Wait, don’t I have to think more with a type alias? I used to be able to see exactly what types were there, and now they’re hidden behind an alias!”

Sure, we’ve introduced one layer of indirection - there’s some detail that’s masked by the alias. But as programmers, we hide details behind names all the time!

  • Instead of writing 9.8 throughout our code, we’d create a constant called ACCELERATION_DUE_TO_GRAVITY.
  • Instead of putting 6.28 * radius, everywhere, we’d put it behind a function called circumference().

Remember - if we need to see what’s behind the alias, it’s just a Command+Click away in the IDE.

Inheritance

Or maybe you’re thinking, “Why would I need a type alias? I could just use inheritance to create a nickname for complex types, like this:”

class Restaurant : Organization<(Currency, Coupon?) -> Sustenance>()

You’re correct - in this case, you could just subclass Organization with its elaborate type argument. You’ve probably seen this done in Java, in fact.

But type aliases also work on types that you can’t or wouldn’t normally inherit from. For example, you can alias:

  • Non-open classes like String, or Java’s Optional<T>
  • Singleton object instances in Kotlin
  • Function types, like (Currency, Coupon?) -> Sustenance
  • And even “Function with Receiver” types, like Currency.(Coupon?) -> Sustenance

We’ll do more of a comparison between a type alias approach and an inheritance approach a little later in this article.

Understanding Type Aliases

We’ve already seen how easy it is to declare a type alias. Now let’s zoom in closer, so we can understand what’s actually happening when we create one!

When dealing with type aliases, we have two types we need to consider:

  • The alias.
  • The underlying type.
Anatomy of a type alias in Kotlin - the left-hand side is the alias, and the right-hand side is the underlying type.

A type that is itself an alias (such as UserId), or that includes an alias (like List<UserId>) is said to be abbreviated.

When Kotlin compiles your code, the abbreviated types are expanded into the full, unabbreviated types everywhere that they’re used. Let’s see a more complete example:

class UniqueIdentifier(val value: Int)

typealias UserId = UniqueIdentifier

val firstUserId: UserId = UserId(0)

When the compiler processes this, all of the references to UserId get expanded into UniqueIdentifier.

In other words, as a general rule, if you were to search your code for all usages of the alias (UserId), and replace them verbatim with the underlying type (UniqueIdentifier), you’d roughly be doing the same thing as the compiler does during expansion.

Before and after for type alias expansion.

You might have noticed I used the words “for the most part” and “roughly”. That’s because, although this is a good starting point for our understanding of type aliases, there are a handful of cases where Kotlin is extra helpful by not doing a completely verbatim replacement. We’ll explore those soon! For now, we’ll just keep in mind that this verbatim replacement guideline is generally helpful.

By the way, if you’re using IntelliJ IDEA, you’ll be glad to know that you get some nifty support for type aliases. For example, you can see both the alias name and the underlying type in code completion:

Screen shot of code completion with a type alias in IntelliJ IDEA.

And in quick documentation:

Screen shot of quick documentation for a type alias in IntelliJ IDEA.

Type Aliases and Type Safety

Now that we’ve got the basics of type aliases down, let’s explore another example. This one makes use of multiple aliases:

typealias UserId = UniqueIdentifier
typealias ProductId = UniqueIdentifier

interface Store {
    fun purchase(user: UserId, product: ProductId): Receipt
}

Once we get an instance of our Store, we can make a purchase:

val receipt = store.purchase(productId, userId)

Hang on! Did you notice something?

We accidentally got our arguments mixed up! The userId is supposed to be the first argument, and the productId is supposed to be the second!

Yowza! Why didn’t the compiler warn us about this?

If we use our verbatim replacement guideline from above, we can simulate the expansion to see how the compiler views this code:

Before and after for type alias expansion for this case - both userId and productId expand to the same underlying type.

Whoa! Both of the parameter types are expanded to the same underlying type! That means it’s possible to mix them up, and the compiler will keep on hummin’ right along.

The big takeaway: Type aliases do not create new types. They simply give another name to an existing type.

That, of course, is why we can alias non-open classes - there’s no subtyping happening.

While you might think this would always be a bad thing, there are actually some situations where it’s helpful!

Let’s compare two different ways of nicknaming a type:

  1. Using a type alias.
  2. Using inheritance to create a subtype (as discussed in the section Inheritance above).

The underlying type in both cases will be a String supplier, which is just a function that takes no argument and returns a String.

typealias AliasedSupplier = () -> String
interface InheritedSupplier : () -> String

Now, let’s create a couple of functions that accept these suppliers:

fun writeAliased(supplier: AliasedSupplier) = 
        println(supplier.invoke())

fun writeInherited(supplier: InheritedSupplier) = 
        println(supplier.invoke())

And finally, we’re ready to call these functions:

writeAliased { "Hello" }
writeInherited { "Hello" } // Zounds! A compiler error!

While the aliased version works just fine with a lambda expression, the inherited version doesn’t even compile! Instead, it gives us this error message:

Required: InheritedSupplier / Found: () -> String

In fact, the only way I’ve found to actually call writeInherited() is to cobble together a verbosity monstrosity like this:

writeInherited(object : InheritedSupplier {
    override fun invoke(): String = "Hello"
})

So in this case, the type alias has an edge over the inheritance-based approach.

Of course, there are times when type safety will be the more important characteristic to you, and in those cases, a type alias might not fit your needs.

Examples of Type Aliases

Now that we’ve got a good grasp on type aliases, let’s take a look at some examples! These will give you some ideas about the kinds of types that you might want to alias:

// Classes and Interfaces
typealias RegularExpression = String
typealias IntentData = Parcelable

// Nullable types
typealias MaybeString = String?

// Generics with Type Parameters
typealias MultivaluedMap<K, V> = HashMap<K, List<V>>
typealias Lookup<T> = HashMap<T, T>

// Generics with Concrete Type Arguments
typealias Users = ArrayList<User>

// Type Projections
typealias Strings = Array<out String>
typealias OutArray<T> = Array<out T>
typealias AnyIterable = Iterable<*>

// Objects (including Companion Objects)
typealias RegexUtil = Regex.Companion

// Function Types
typealias ClickHandler = (View) -> Unit

// Lambda with Receiver
typealias IntentInitializer = Intent.() -> Unit

// Nested Classes and Interfaces
typealias NotificationBuilder = NotificationCompat.Builder
typealias OnPermissionResult = ActivityCompat.OnRequestPermissionsResultCallback

// Enums
typealias Direction = kotlin.io.FileWalkDirection
// (but you cannot alias a single enum *entry*)

// Annotation
typealias Multifile = JvmMultifileClass

Cool Things You Can Do With Type Aliases

Undercover agent.

As we’ve seen, once you create an alias, you can use it instead of the underlying type in a variety of scenarios, like:

In addition to these, there are a few uses that warrant some extra detail. Let’s take a look!

Constructors

If the underlying type has a constructor, so will the type alias. You can even invoke the constructor on an alias of a nullable type! For example:

class TeamMember(val name: String)
typealias MaybeTeamMember = TeamMember?

// Constructing with the alias:
val member =  MaybeTeamMember("Miguel")

// The above code does *not* expand verbatim to this (which wouldn't compile):
val member = TeamMember?("Miguel")

// Instead, it expands to this:
val member = TeamMember("Miguel")

So as you can see, the expansion is not always verbatim, which is helpful here.

If the underlying type has no constructors (such as an interface or a type projection) then you can’t invoke a constructor on the alias either. Naturally.

Companion Object

You can also invoke properties and functions on a companion object using an alias. This works even if the underlying type has a concrete type argument specified. Check it out:

class Container<T>(var item: T) {
    companion object {
        const val classVersion = 5
    }
}

// Note the concrete type argument of String
typealias BoxedString = Container<String>

// Getting a property of a companion object via an alias:
val version = BoxedString.classVersion

// The line above does *not* expand to this (which wouldn't compile):
val version = Container<String>.classVersion

// Instead, it expands to this:
val version = Container.classVersion

Again, we see that Kotlin doesn’t always do verbatim replacement, especially in cases where it’s helpful to do something else.

Gotchas

Agent wearing suit and shades.

There are a few other things to keep in mind as you use type aliases.

Top-Level Only

Type aliases are top-level only. In other words, they can’t be nested inside a class, object, interface, or other code block. If you try to do this, you’ll get this error message from the compiler:

Nested and local type aliases are not supported.

However, you can restrict their visibility with the usual visibility modifiers like internal and private. So if you want a type alias to be accessible only from within one class, you’d need to put the type alias and the class in the same file, and mark the alias as private, like this:

private typealias Message = String

object Messages {
    val greeting: Message = "Hello"
}

Interestingly, the private type alias can appear in a public position, as it does above, where greeting: Message is public.

Java Interoperability

How do you use a Kotlin type alias from Java code?

You don’t. They aren’t visible from Java.

But if you have Kotlin code that references a type alias, like this…

typealias Greeting = String

fun welcomeUser(greeting: Greeting) {
    println("$greeting, user!")
}

…then your Java code can continue to interact with it by using the underlying type, like this…

// Using type String here instead of the alias Greeting
String hello = "Hello";
welcomeUser(hello);

Recursive Aliases

It’s totally fine to make an alias of an alias:

typealias Greeting = String
typealias Salutation = Greeting 

However, you obviously can’t have a recursive type alias definition:

typealias Greeting = Comparable<Greeting>

The compiler would error out with this message:

Recursive type alias in expansion: Greeting

Type Projections

If you create a type projection, be careful about your expectations. For example, if we have this code:

class Box<T>(var item: T)
typealias Boxes<T> = ArrayList<Box<T>>

fun read(boxes: Boxes<out String>) = boxes.forEach(::println)

… then we might expect this to work:

val boxes: Boxes<String> = arrayListOf(Box("Hello"), Box("World"))
read(boxes) // Oops! Compiler error here.

The problem is that Boxes<out String> expands to ArrayList<Box<out T>>, not to ArrayList<out Box<out T>>.

Import As: The Cousin of Type Alias

There’s a concept that’s very to type aliases, called Import As. It allows you to give a new name to a type, function, or property when you import it into a file. For example:

import android.support.v4.app.NotificationCompat.Builder as NotificationBuilder

In this case, we’ve imported the Builder class from NotificationCompat, but in the current file, it’ll go by the name NotificationBuilder.

Have you ever run into a situation where you need to import two classes that have the same name?

If so, then you can imagine how Import As can be a huge help, because it means you don’t have to qualify one of those classes.

For example, check out this Java code, where we translate a database model of a user to a service model of a user.

package com.example.app.service;

import com.example.app.model.User;

public class UserService {
    public User translateUser(com.example.app.database.User user) {
        return new User(user.getFirst() + " " + user.getLast());
    }
}

Since this code deals with two different classes, each called User, we can’t import them both. So instead, we end up fully qualifying one of them.

With Kotlin’s Import As, we don’t have to fully qualify it - we can just give it another name when we import it!

package com.example.app.service

import com.example.app.model.User
import com.example.app.database.User as DatabaseUser

class UserService {
    fun translateUser(user: DatabaseUser): User =
            User("${user.first} ${user.last}")
}

You might be wondering, “Well, then…. what’s the difference between using a type alias and Import As? After all, you could also disambiguate the User references with typealias, like this:”

package com.example.app.service

import com.example.app.model.User

typealias DatabaseUser = com.example.app.database.User

class UserService {
    fun translateUser(user: DatabaseUser): User =
            User("${user.first} ${user.last}")
}

That’s correct. In fact, as it so happens, other than the metadata, these two versions of UserService compile down to the same bytecode!

So why would you choose one over the other? What are the differences? Here’s a list of different things you might want to alias or import, and whether they’re supported by each:

Target Type Alias Import As
Interfaces and Classes Yes Yes
Nullable Types Yes No
Generics with Type Params Yes No
Generics with Type Arguments Yes No
Function Types Yes No
Enums Yes Yes
Enum Members No Yes
object Yes Yes
object Functions No Yes
object Properties No Yes

As you can see, some targets are only supported by one or the other.

Here are a few other things to keep in mind:

  • Type aliases can have visibility modifiers like internal and private, whereas imports will be file-scoped.
  • If you import a class from a package that’s already automatically imported, like kotlin.* or kotlin.collections.*, then you have to reference it by that name. For example, if you were to write import kotlin.String as RegularExpression, then usages of just String would refer to java.lang.String. Yikes!

By the way, if you’re an Android developer and you’re using Kotlin Android Extensions in your project, Import As is a fantastic way to map those snake_cased XML IDs to camelCased references that look like the rest of the variables in your activity:

import kotlinx.android.synthetic.main.activity.upgrade_button as upgradeButton

This can make your transition from findViewById() (or Butter Knife) to Kotlin Android Extensions very easy!

Wrap-up

Type aliases can be a great way to take complex, verbose, and abstract types and give them simple, concise, and domain-specific names. They’re easy to use, and the tooling support gives you insight into the underlying types. Used in the right place, they can make your code easier to read and understand.

Share this article:

  • Share on Twitter
  • Share on Facebook
  • Share on Reddit