Type erasure using closures in Swift
One of the things that makes Swift so much safer and less error-prone than many other languages is its advanced (and in a way, unforgiving) type system. It’s one of the language features that can at times be really impressive and make you a lot more productive, and at other times be really frustrating.
Today, I wanted to highlight one type of situation that can occur when dealing with generics in Swift, and how I usually solve it using a type erasure technique based on closures.
Let’s say we want to write a class that lets us load a model over the network. Since we don’t want to have to replicate this class for every single model in our application, we choose to make it a generic, like this:
So far so good, we now have a ModelLoader that is capable of loading any model, as long as it’s Unboxable, and is able to give us a requestURL. But, we also want to enable code that uses this model loader to be easily testable, so we extract its API into a protocol:
This, together with dependency injection, enables us to easily mock our model loading API in tests. But it comes with a bit of a complication — in that whenever we want to use this API, we now have to refer to it as the protocol ModelLoading, which has an associated type requirement. This means that simply referring to ModelLoading is not enough, as the compiler cannot infer its associated types without more information. So trying to do this:
will give us this error:
Protocol ‘ModelLoading’ can only be used as a generic constraint because it as Self or associated type requirements
But no worries, we can easily get rid of this error by using a generic constraint, to enforce that the concrete type conforming to ModelLoading will be specified by the API user, and that it will load the kind of model that we’re expecting. Like this:
This works, but since we also want to have a reference to our model loader in our view controller, we need to be able to specify what type that property will be. T is only known in the context of our initializer, so we can’t define a property with type T, unless we make the view controller class itself a generic — which will quite quickly make us fall further and futher down into a rabit hole of generic classes everywhere.
Instead, let’s use type erasure, to enable us to save some kind of reference to T, without actually using its type. This can be done by creating a type erased type, such as a wrapper class, like this:
The above is a type erasure technique that is also quite commonly used in the Swift standard library, for example in the AnySequence type. Basically you wrap a protocol that has associated value requirements into a generic type, which you can then use without having to keep making everything using it also be generic.
We can now update our ViewController from before, to use AnyModelLoader:
Done! We now have a protocol-oriented API, with easy mockability, that can still be used in a non-generic class, thanks to type erasure.
Now, time for the bonus round. The above technique works really well, but it does involve an extra step that adds a bit of complication to our code. But, turns out, we can actually do the closure-based type erasure directly in our view controller — instead of having to introduce the AnyModelLoader class. Then, our view controller would instead look like this:
As with our type erased class AnyModelLoader, we can refer to the implementation of the load function as a closure, and simply save a reference to it in our view controller. Now, whenever we want to load a model, we just call loadModel, like we would any other function or closure:
That’s it! Hope you’ll find the above techniques useful when dealing with generics and protocols in your Swift code. Questions, feedback or comments are more than welcome — both here and on Twitter. You can find me @johnsundell.
Thanks for reading! 🚀
I write weekly blog post about Swift development here on Medium, and also work on several Swift open source projects on GitHub. You can also follow me on Twitter for updates on all my Swift adventures.