Have you ever had a conversation like this?
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.
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 calledACCELERATION_DUE_TO_GRAVITY
. - Instead of putting
6.28 * radius
, everywhere, we’d put it behind a function calledcircumference()
.
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 likeString
, or Java’sOptional<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.
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.
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:
And in quick documentation:
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:
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:
- Using a type alias.
- 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
As we’ve seen, once you create an alias, you can use it instead of the underlying type in a variety of scenarios, like:
- When declaring the type of variables, parameters, and return types.
- As type parameter constraints and type arguments.
- When comparing instance types with
is
or casting withas
. - When getting function references.
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
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
andprivate
, whereas imports will be file-scoped. - If you import a class from a package that’s already automatically imported, like
kotlin.*
orkotlin.collections.*
, then you have to reference it by that name. For example, if you were to writeimport kotlin.String as RegularExpression
, then usages of justString
would refer tojava.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.