A Swift walk through type erasure

If you’re writing Swift then chances are you’ve heard about type erasure. Maybe you’re even using it. But there’s a chance that even if you fall into one of these two camps you may not actually know why type erasure works. 🤷🏽‍♂️

Rather than give you a bullet to bullet how-to for type erasure let’s actually go through what the problem is and work our way up to the solution.

The Problem

Let’s suppose we were making a generic network session class that had to support a variety of JSON deserializers that returned an object matching the associated type of the network session.
Something like this:

Drop this in a playground and what you’ll see is the dreaded Protocol 'JsonDeserializer' can only be used as a generic constraint because it has Self or associated type requirements .

This is annoying, but it does make sense. Our protocol JsonDeserializer is an abstract type so the Swift compiler can’t infer which concrete type it refers to nor can it assume the type supplied by its associatedtype at compile time.

Hector Matos says it well in his blog KrakenDev :

The short answer is: Swift wants to be type-safe. Couple that with the fact that it’s an ahead-of-time compiled language, and you have a language that NEEDS to be able to infer a concrete type at anytime during compilation. I can’t stress that enough. At compile time, every one of your types that aren’t function/class constraints need to be concrete. Associated types within a protocol are abstract. Which means they aren’t concrete. They’re fake. And no one likes a fake.

So while this error is irritating, it does make sense — and the error tells us something important. As it points out, the protocol can only be used as a generic constraint, which means between angle brackets <>.

Attempt 1

We know that the only way to use the JsonDeserializer as type constraint is to use it within <>, but let’s say for arguments sake we decide to specify a type with the protocol constraint much like how we’d do it for a generic class. What’s the result?

Cannot specialize non-generic type 'JsonDeserializer'

This was expected though. As we discussed earlier, the protocol is an abstract type and while it can have a generic requirement, it itself is not a generic type. Not the way class SomeType<T> { ... } is.

Attempt 2

Alright, well let’s try using JsonDeserializer as a generic constraint and toss the deserializer in to a proxy object which we’ll call DeserializerBox (the use of Box here is for consistency with type erasure naming convention later).

Here’s the result of that decision:
generic type 'DeserializerBox' requires arguments in <...>

This error is telling us we need to supply a type to satisfy the generic type requirement of DeserializerBox. Unfortunately that means specifying a JsonDeserializer. But, we want to use any JsonDeserializer that supplies us with a Payload type object, so to specify a specific concrete type of deserializer would defeat the purpose. That would be no different from just using the concrete type as the type constraint on our Session's deserializer property. Discouraging as it is, this is progress.

Attempt 3 and 3.5ish

To top off our lack of required type arguements, we also need DeserializerBox to be a true proxy object for JsonDeserializer, which means it should indistinguishable from a JsonDeserializer. This means we should be able to treat the proxy the same as a JsonDeserializer and it should expose the same interface. To do this we’ll have to adhere to JsonDeserializer, wrap up an internal concrete JsonDeserializer, and trampoline protocol properties and method calls to the concrete deserializer.

But, we’re not done… We saw in the last attempt that using JsonDeserializer as a generic constraint required that we supply a concrete type to our Box object which is counter productive, so instead let’s try using a Payload type, say T, as our generic constraint and let’s adhere to JsonDeserializer.

Without messing with the DeserializerBox (we’ll come back to this) let’s define a simple wrapper around a concrete deserializer:

But we quickly encounter errors:

We’re, again, referring to JsonDeserializer without using it as a generic constraint so we know this will fail. Let’s update that to use generic constraints and see what happens:

<T, D: JsonDeserializer> is now exposed, which is probably not a great idea. By exposing that constraint we will inevitably be in the same place as attempt 2 where we’d have to specify a specific JsonDeserializable to satisfy the generic requirements. If only we could bury that requirement somewhere… Let’s talk about it in attempt 4.

Attempt 4 — Combining Ideas💡

What we know:

  • We need to be able to store a deserializer which means we need to have the generic constraint <D: JsonDeserializer>
  • But we need to bury the fact that the Box has generic requirements and we need to expose the proxy object like we did with V1 ofSomeDeserializer where all we know is that is has a generic type <T>

If only the DeserializerBox, which does a great job of concealing its internal deserializer, could have the signature and JsonDeserializer trampolining behavior of the SomeDeserializer<T>. But it can!

The scenario we’re describing is inheritance (subclassing)! We just have to define an abstract class, say BaseJsonDeserializer<T>: JsonDeserializer, which when subclassed by DeserializerBox will endow it with all the overridable behavior of a JsonDeserializer AND to top it allows the DeserializerBox<D: JsonDeserializer> to be tucked away behind it’s super class declaration BaseJsonDeserializer<T>
Let’s go ahead and define these classes:

Clean Up

We now have a mechanism for which we can refer to any JsonDeserializer of a particular expected Payload type. We’d simply store the instance as a BaseJsonDeserializer<Payload>righhht?

Well, if you’re familiar with the type erasure pattern, then you already know that typically there are three components:

  • Abstract base class ✅
  • Private box ✅ (ours isn’t private, yet)
  • Public wrapper ❓

The third component is a response to our question, why not just use BaseJsonDeserializer<Payload>? The BaseJsonDeserializer class is an abstract class and as an abstract class should never be initialized directly as the abstract class is only there to set up an inheritable structure for its subclasses. For this reason, the BaseJsonDeserializer should not be our public facing object — we should take care not to expose a class publicly if it shouldn’t be used directly.

And that’s where the Public wrapper comes into play. Throughout this post I’ve been referring to a proxy object — which is a result of the proxy design pattern. Our Public wrapper which we’ll call AnyJsonDeserializer is going to be our actual proxy object. It will adhere to JsonDeserializer, giving it the same interface as a concrete JsonDeserializer, but will actually forward/trampoline method and property calls to its DeserializerBox object. Implementing this last component, our final code looks like:

Resources 📚

So there we have it, type erasure from start to finish 🏆. Using type erasure is great but understanding how and why it works is even better!

If you liked what you read feel free to like, share, and subscribe 🙌🏾
Thanks for reading!

🐶 ☮️ ❤️

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.