Typealias: Swift's underestimated champion

I've been aware of typealias for a long time. It's only recently that I really started to understand just how powerful this keyword can be. Let me share why I think they are the underestimated champion for unit tests and dependency injection.

Reference Use Case

A class depends on the services of another class. Your first instinct will likely be to create a protocol that will define the interface that you will require.

struct GroceryList: Codable {
    var items: [String]
}

protocol GroceryListLoader() {
    func loadFromURL(_ url: URL) throws -> GroceryList
}

class MyGroceryListManager {
    let loader: GroceryListLoader
    var groceryLists = [Name: GroceryList]()

    init(loader: GroceryListLoader) {
        self.loader = loader
    }

    func openList(name: String) throws {
        let listUrl = URL(string: "file://path/to/\(name).json")!
        groceryLists[name] = try loader.loadFromUrl(listUrl)
    }
}

This allows you to replace the implementation of the GroceryListLoader with a mock/stub during unit tests. It also makes your code more flexible as it can work with any number of implementations as long as they can conform to the GroceryListLoader protocol.

Reference Case Unit Test

Let's create a small unit test to demonstrate how this is typically implemented:

class GroceryListLoaderMock: GroceryListLoader {
    enum Errors: Error {
      case fileNotFound
    }

    var lists = [URL: GroceryList]()
    func loadFromURL(_ url: URL) throws -> GroceryList {
        guard let list = lists[url] else {
            throw Errors.fileNotFound
        }
        return list
    }
}

class GroceryListManagerTests: XCTestCase {
    func testThrowsWhenFileIsMissing() {
        let mockLoader = GroceryListLoaderMock()
        let manager = GroceryListManager(loader: mockLoader)
        XCTAssertThrow(try manager.openList(name: "Walmart"))
    }
}

Nothing unusual here. When executing testThrowsWhenFileIsMissing() a mockLoader is created, and since no list was assigned to the mock's lists, the loadFromURL() function will throw the Errors.fileNotFound. In turn the GroceryListManager implementation of openList() because it doesn't capture any error throw by the loader, will simply allow the thrown error to carry on to the caller of the openList() function therefore passing the test.

Typealias alternative

struct GroceryList: Codable {
    var items: [String]
}

typealias LoadGroceryList = (_ url: URL) throws -> GroceryList

class GroceryListManager {
    let loader: LoadGroceryList
    var groceryLists = [Name: GroceryList]()

    init(loader: @escaping LoadGroceryList) {
        self.loader = loader
    }

    func openList(name: String) throws {
        let listUrl = URL(string: "file://path/to/\(name).json")!
        groceryLists[name] = try loader(listUrl)
    }
}

Here, instead of defining a protocol, we define a typealias with the signature of the closure we need. In the GroceryListManager we will store a reference to that closure, ensuring we tag it as @escaping since it will be used outside the scope of init(). Finally, in openList() we execute the closure to receive the list at the given URL.

Typealias Unit Test

class GroceryListManagerTests: XCTestCase {
    func testThrowsWhenFileIsMissing() {
        enum Errors: Error {
           case fileNotFound
        }
        let mockLoader: LoadGroceryList = { _ in throw Errors.fileNotFound }
        let manager = GroceryListManager(loader: mockLoader)
        XCTAssertThrow(try manager.openList(name: "Walmart"))
    }
}

Because the typealias defines the closure signature needed to fulfill the requirements needed by GroceryListManager class, we no longer need a mock class to be created. We can directly implement a custom implementation needed in the closure.

It could even be simplified to the following:

class GroceryListManagerTests: XCTestCase {
    func testThrowsWhenFileIsMissing() {
        enum Errors: Error {
           case fileNotFound
        }
        let manager = GroceryListManager(
            loader: { _ in throw Errors.fileNotFound })
        XCTAssertThrow(try manager.openList(name: "Walmart"))
    }
}

As you can see, the unit test is much easier to comprehend, as we no longer depend on the specific implementation of the Mock.

Declaring conformance in another class

In reality, many of your classes may expose multiple functions. You can still be empowered to use typealias, for example:

typealias LoadGroceryList = (URL) throws -> GroceryList

class MyGroceryListLoader {
   let loadFromUrl: LoadGroceryList = loadGroceryList

   private func loadGroceryList(_ url: URL) throws -> GroceryList {
     ...
   }
}

You can also define protocol conformance like you used to if needed:

typealias LoadGroceryList = (URL) throws -> GroceryList
typealias WriteGroceryList = (GroceryList, URL) throws -> Void

protocol GroceryListLoaderWriter {
   var loadFromURL: LoadGroceryList { get }
   var writeToURL: WriteGroceryList { get }
}

class MyGroceryListLoaderWriter: GroceryListLoaderWriter {
   let loadFromURL: LoadGroceryList = loadGroceryList
   let writeToURL: WriteGroceryList = writeGroceryList

   private func loadGroceryList(_ url: URL) throws -> GroceryList {
      ...
   }

   private func writeGroceryList(_ groceryList: GroceryList, _ url: URL) throws {
      ...
   }
}

class MyGroceryListManager {
   let loader: GroceryListLoaderWriter
   var openedLists = [Name: GroceryList]()

   init(loader: GroceryListLoaderWriter) {
      self.loader = loader
   }

   func openList(name: String) throws {
      let listUrl = URL(string: "/path/to/\(name).json")!
      openedLists[name] = try loader.loadFromUrl(listUrl)
   }
}

let myLoaderWriter = MyGroceryListLoaderWriter()
let myManager = MyGroceryListManager(loader: myLoaderWriter)

In the above case however, we can notice that MyGroceryListManager doesn't use the writer, only the reader, so it can be simplified to:

class MyGroceryListManager {
   let loader: LoadGroceryList
   var openedLists = [Name: GroceryList]()

   init(loader: @escaping LoadGroceryList) {
      self.loader = loader
   }

   func openList(name: String) throws {
      let listUrl = URL(string: "/path/to/\(name).json")!
      openedLists[name] = try loader(listUrl)
   }
}

let myLoaderWriter = MyGroceryListLoaderWriter()
let myManager = MyGroceryListManager(loader: myLoaderWriter.loadFromUrl)

Conclusion

Using typealias can provide you with additional flexibility and may end up requiring fewer lines of code in your app and in your unit tests.

If your class depends on the entirety of the protocol, then please continue to use your protocols like you are used to. However, if your class depends only on a subset of your protocol, a typealias may just be the tool that will allow you to simplify your unit tests and your dependency injections.