What is a typealias
?
When thinking about the great language features of Swift, few people think about the typealias
. However, there're many situations where a typealias can become useful. This article will give a brief introduction of what a typealias
is, how you define it, and list multiple examples of how you can use them in your own code. Lets dive in.
A typealias
is - as the name implies - an alias for a specific type. Types, such as Int
, Double
, UIViewController
, or one of your custom types. A Int32
and a Int8
are different types. A type alias, on the other hand, inserts a second name for an existing type into your codebase. For example:
typealias Money = Int
Creates an alias for the Int
type. With this, you can use Money
as if it were Int
everywhere in your code:
struct Bank {
typealias Money = Int
private var credit: Money = 0
mutating func deposit(amount: Money) {
credit += amount
}
mutating func withdraw(amount: Money) {
credit -= amount
}
}
Above, we have a struct Bank
that manages money. Instead of using Int
for our amounts, though, we use our Money
type. Observe that the +=
and -=
operators still work as expected.
You can also mix and match type aliases and the original types. This is possible because, to the Swift compiler, they all resolve to the same thing:
struct Bank {
typealias DepositMoney = Int
typealias WithdrawMoney = Int
private var credit: Int = 0
mutating func deposit(amount: DepositMoney) {
credit += amount
}
mutating func withdraw(amount: WithdrawMoney) {
credit -= amount
}
}
Here, we're mixing Int
and our different custom type aliases DepositMoney
and WithdrawMoney
.
Generic Type aliases
In addition to the above, a type aliase can also have generic parameters:
typealias MyArray<T> = Array<T>
let newArray: MyArray = MyArray(arrayLiteral: 1, 2, 3)
Above, we defined a typealias for MyArray
that works just like the normal array. Finally, the generic parameters of your aliased types can even have constraints. Imagine that we want our new MyArray
to only hold types that conform to StringProtocol
:
typealias MyArray<T> = Array<T> where T: StringProtocol
This is already a nice feature as you can quickly define arrays for particular types without having to subclass Array
. With that said, let us look at some practical applications of these typealias types.
Practical Applications
Clearer Code
The first, and obvious, use case is something we already briefly touched on. A type alias can give your code more meaning. In our example typealias Money = Int
we introduced a clear concept of what the Money
type is. Using it like let amount: Money = 0
is much more understandable than let amount: Int = 0
. In the first example, you know immediately that this is an amount of money. In the second example, it could be anything: An amount of bikes, an amount of characters, an amount of donuts - who knows!
Obviously, this is not always necessary. If your function signature already clearly explains the type of the parameter (func orderDonuts(amount: Int)
) then it would be an unnecessary overhead to include another typealias. On the other hand, for variables and constants, it oftentimes improves readability and tremendously improves the documentation.
Simpler Optional Closures
Optional closures in Swift are a wee bit unwieldy. The normal definition of a closure accepting one Int
parameter and returning Int
looks like this:
func handle(action: (Int) -> Int) { ... }
Now, if you want to make this closure optional, you can't just add a questionmark:
func handle(action: (Int) -> Int?) { ... }
After all, this is not an optional closure but instead a closure that returns optional Int
. The right way to do this is by adding parentheses:
func handle(action: ((Int) -> Int)?) { ... }
This becomes especially ugly if you have multiple of such actions. Below, have have a function that handles a success and failure case, as well as calling an additional closure with the progress of the operation.
func handle(success: ((Int) -> Int)?,
failure: ((Error) -> Void)?,
progress: ((Double) -> Void)?) {
}
This small section of code contains a lot of parentheses. As we're not aiming to become lispers, we'd like to address this by using typealiases for the different closures:
typealias Success = (Int) -> Int
typealias Failure = (Error) -> Void
typealias Progress = (Double) -> Void
func handle2(success: Success?, failure: Failure?, progress: Progress?) { ... }
The actual function does look much more readable. While this is good, we did introduce additional syntax through three lines of typealias
. This, however, might actually help us in the long run, as we will see next.
Centralizing Defintions
The action handlers in our previous example might not be the only place where this specific type is used. Here's a draft of what a slightly modified class would look like that actually uses the action handler:
final class Dispatcher {
private var successHandler: ((Int) -> Void)?
private var errorHandler: ((Error) -> Void)?
func handle(success: ((Int) -> Void)?, error: ((Error) -> Void)?) {
self.successHandler = success
self.errorHandler = error
internalHandle()
}
func handle(success: ((Int) -> Void)?) {
self.successHandler = success
internalHandle()
}
func handle(error: ((Int)-> Void?)) {
self.errorHandler = error
internalHandle()
}
private func internalHandle() {
...
}
}
This struct introduces two closures, one for the success and one for the error case. However, we also want to offer convenience functions to call with only the one or the other handler. In the example above, if we want to add another parameter to the success and error handler, say the HTTPResponse
, we'll need to update a lot of code. ((Int) -> Void)?
would need to become ((Int, HTTPResponse) -> Void)?
in three places. Similarly for the errorHandler. By using multiple typealiases, we can circumvent this and only have to modify the type in one place:
final class Dispatcher {
typealias Success = (Int, HTTPResponse) -> Void
typealias Failure = (Error, HTTPResponse) -> Void
private var successHandler: Success?
private var errorHandler: Failure?
func handle(success: Success?, error: Failure?) {
self.successHandler = success
self.errorHandler = error
internalHandle()
}
func handle(success: Success?) {
self.successHandler = success
internalHandle()
}
func handle(error: Failure?) {
self.errorHandler = error
internalHandle()
}
private func internalHandle() {
...
}
}
Not only is this much easier to read, it will also continue to be helpful as we introduce the type in more places.
Generic Aliases
A typealias can also be generic. One simple use case would be to enforce a container with a special meaning. Say we have an app that processes books. A book consists out of chapters, chapters consist out of pages. Fundamentally, those are just arrays though. typealias
to the resuce:
struct Page {}
typealias Chapter = Array<Page>
typealias Book = Array<Chapter>
This has two benefits compared to just using a array.
- The code is more explanatory
- The array that houses the pages can only contain pages. Nothing else.
Coming back to our earlier example of using success and failure handlers, we can improve this even more by using a generic handler:
typealias Handler<In> = (In, HTTPResponse?, Context) -> Void
func handle(success: Handler<Int>?,
failure: Handler<Error>?,
progress: Handler<Double>?,)
This composes really well and allows us to write a simpler function, and have one place where we edit the Handler
.
This approach is also very useful with your own types. You can create one generic definition and then define detailed typealiases:
struct ComputationResult<T> {
private var result: T
}
typealias DataResult = ComputationResult<Data>
typealias StringResult = ComputationResult<String>
typealias IntResult = ComputationResult<Int>
Again, the typealias allows us to write less code and simplifies our definitions.
Tuples like Functions
Similarly, you can use generics and tuples to define types without having to resort to structs. Below, we envision the datatype for a genetic algorithm that modifies its value T
over multiple generations.
typealias Generation<T: Numeric> = (initial: T, seed: T, count: Int, current: T)
If you define a typealias like this, you can actually initialize it like you would initialize a struct:
let firstGeneration = Generation(initial: 10, seed: 42, count: 0, current: 10)
While this does look like a struct, it is just a type alias for a tuple.
Combining Protocols
Sometimes you end up in a situation where you have multiple protocols and there is one specific type that should implement them all. Usually this happens when you define a protocol hierachy in order to provide more flexibility.
protocol CanRead {}
protocol CanWrite {}
protocol CanAuthorize {}
protocol CanCreateUser {}
typealias Administrator = CanRead & CanWrite & CanAuthorize & CanCreateUser
typealias User = CanRead & CanWrite
typealias Consumer = CanRead
Here, we define a permission hierachy. The administrator can do everything, a user can read and write, and a consumer can only read.
Associated Types
This goes beyond the scope of this article, but the associated types of protocols are also defined via type aliases:
protocol Example {
associatedtype Payload: Numeric
}
struct Implementation: Example {
typealias Payload = Int
}
Drawbacks
While typealiases are generally a very useful feature, they have one small drawback: If you're new to a codebase, then there's an important difference between these two definitions:
func first(action: (Int, Error?) -> Void) {}
func second(action: Success) {}
The second one is not immediately obvious. What kind of type is Success
? How do you construct it? You'll have to option-click it in Xcode in order to understand what it does and how it works. This causes additional overhead. If you use many typealiases, this will take even more time. There's no good solution to this, except that it (as so often) depends on the usecase.