Weekly Swift articles, podcasts and tips by John Sundell.

The power of type aliases in Swift

Published on 20 Jan 2019
Basics article available: Generics

Whether or not syntactic sugar and other mostly cosmetic code changes actually add value, or whether they cause confusion and complexity, is a common source of debate among developers. While being able to write code in a more concise manner can lead to increased productivity, it can also sometimes make the result harder to read and maintain.

What we ideally want is to be able to strike a nice balance between low verbosity and clarity, and to make good use of the various features that Swift’s type system offers to make that happen. One such feature is type aliases, and this week, let’s take a look at a few different ways that they can be used to create new, focused, types in a very lightweight manner.

Semantic types

In general, embedding semantics into our types can make a big difference in making code more intuitive and easier to work with. This is especially true when dealing with primitive types, such as numbers and strings, since they can be used for almost anything. When we see an Int, Double or String in a function signature — we usually have to rely on the name of that function, and its parameters, in order to understand what that value will be used for.

Take Foundation’s TimeInterval type as an example. Whenever we see a time interval, we know that we’re dealing with time (or more specifically, seconds), which wouldn’t be true if a “raw” number type like Double was used instead. However, it turns out that TimeInterval is actually not a brand new type, and is in fact just a type alias for Double:

typealias TimeInterval = Double

Using a type alias like that has pros and cons. Since TimeInterval isn’t actually its own type, but rather just an alias — it means that all Double values are valid TimeInterval values, and vice versa. The downside of that is that we don’t get that extra compile time safety that we would if we were creating a brand new type instead, but on the other hand, the benefit is that any Double method (including operators) can also be used on TimeInterval values — reducing the need for code duplication and boilerplate.

So how can we do the same, but for our own types? Like we took a look at in “Writing self-documenting Swift code”, type aliases can be a great way to make our code become more self-documenting. For example, inspired by TimeInterval, we could define a Kilograms type alias — that’ll let us easily express what unit of measurement a certain weight value is using — like this:

typealias Kilograms = Double

struct Package {
    var weight: Kilograms
}

Doing something like the above might seem insignificant, but can be a great way to make our properties and their values extra clear — without having to rely on documentation, or by using very verbose names for our properties, like weightInKilograms.

Specializing generics

Type aliases also provide an easy way to specialize generics, especially those that are used with the same generic types throughout our code base. Let’s say that we have created a FileStorage type that is used to work with either a local or remote file system — such as iCloud Drive or Dropbox:

class FileStorage<Key: Hashable, Location: FileStorageLocation> {
    ...    
}

Using such a type can be really convenient, as it lets us contain all of the code needed to deal with any kind of file system in one place, while still allowing for specialization at the call site. For example, a NoteSyncController might use two instances of the above class — one to keep track of all the notes stored on the user’s local device, and one for uploading files to the cloud, like this:

class NoteSyncController {
    init(localStorage: FileStorage<Note.StorageKey, LocalFileStorageLocation>,
         cloudStorage: FileStorage<Note.StorageKey, CloudStorageLocation>) {
        ...
    }
}

However, having to type those long FileStorage specializations every time we’re using them can quickly become quite tedious, and make our code harder to read — especially when many such specializations are used in the same place, like above.

This is another situation in which type aliases can become super useful, since they essentially let us do that specialization once — and create dedicated, lightweight types for each use case. In this scenario, we could extend our Note model to contain two such type aliases, one for LocalStorage and one for CloudStorage:

extension Note {
    typealias LocalStorage = FileStorage<StorageKey, LocalFileStorageLocation>
    typealias CloudStorage = FileStorage<StorageKey, CloudStorageLocation>
}

With the above in place, we’ll now be able to heavily clean up our NoteSyncController initializer from before, making it a lot easier to read:

class NoteSyncController {
    init(localStorage: Note.LocalStorage,
         cloudStorage: Note.CloudStorage) {
        ...
    }
}

Making the above change also hides implementation details in a way. While those details are still accessible when needed, we no longer have to clutter up our function signatures or property types with details like what type of key that our storage types are using. Much cleaner! 👍

Type-driven logic

Like we took a look at in Specializing protocols in Swift, using associated types allows us to create generic protocols that can then later be specialized — either by using protocol extensions with constraints, or by making various concrete types conform to them.

For example, in order to work with indexes in a uniform way throughout our code base, we might create a generic Index type — that can then be specialized through an Indexed protocol. Since Index can only be used with types that conform to Indexed, we’re able to use its RawIndex type to determine what underlying value our index is made up of:

protocol Indexed {
    associatedtype RawIndex
    var index: Index<Self> { get }
}

struct Index<Object: Indexed> {
    typealias RawValue = Object.RawIndex
    let rawValue: RawValue
}

With the above setup, we can now use type aliases to declare how we want each type to be indexed. For example, a User might be indexed based on its identifier, while an Album (if we’re building a music app) could be indexed by the music genre that it belongs to:

extension User: Indexed {
    typealias RawIndex = Identifier<User>
}

extension Album: Indexed {
    typealias RawIndex = Genre
}

The cool thing about the above is that we’re now able to create completely type-safe indexes, that use the information provided by our above type aliases to ensure that the correct raw value is used:

let albumIndex = Index<Album>(rawValue: .rock)

If the above looks familiar, it’s because it’s very similar to the technique used to implement more robust identifiers in “Type-safe identifiers in Swift”.

Generic closure aliases

Finally, let’s take a look at how type aliases can be used to create generic shorthands for closures. For example, if our code base makes heavy use of a Result type to model various results of asynchronous operations — we might want to define a shorthand for a closure that takes such a result, which is what we’ll most likely use for lots of our completion handlers:

typealias Handler<T> = (Result<T>) -> Void

Now, whenever one of our functions accepts a completion handler, we can simply use our above Handler type and specialize it with whichever result that we’ll be passing into that handler — like this:

func searchForNotes(matching query: String,
                    then handler: @escaping Handler<[Note]>) {
    ...
}

Again, the above change might seem like purely a cosmetic one, but it could make a big impact on how fluid our code is to both read and write — especially as our code base grows, and we end up with a large number of function declarations that make use of completion handlers.

Conclusion

Type aliases is one of those Swift features that might seem very simple at first, but once we dive in and take a closer look, it turns out that they’re quite capable in many kinds of situations. While over-using them could make our code base harder to navigate (in case we constantly need to look up what concrete type is behind each alias), making selective use of them can lead to more elegant and simplified code.

What do you think? Do you currently use type aliases in your projects, have you tried using them like in this article, or is that something you’ll try out? Let me know — along with your questions, comments and feedback — on Twitter @johnsundell.

Thanks for reading! 🚀