Type erasure is a method to abstract and encapsulate heterogenous generic types inside a single non-generic concrete type. In programming languages with generic types, this is a mechanism for runtime polymorphism which allows you circumvent the constraints of generics at compile time. In the early days of Swift, generics were more challenging to work with — in particular, protocols with associated types. As of Swift 5.7 (and SE-0309: Unlock existentials for all protocols), type erasure is now a feature of the Swift compiler. However, there are still situations where you may need to manually write your own type-erased wrappers.

In This Series

This post is part of a series about ReactiveCollectionsKit.

  1. Introducing ReactiveCollectionsKit
  2. Diffing in ReactiveCollectionsKit
  3. Type erasure for Equatable and Hashable types in Swift

In this post, I’m going to use ReactiveCollectionsKit as a case study for some of the challenges you may face with generic programming in Swift and how we can use type erasure to solve them. I’d recommend reading the other posts in my series on ReactiveCollectionsKit.

The idea behind type erasure is that you need to “erase” the type information for multiple heterogenous types in order to refer to them as a single homogenous type. In other words, type erasure allows you to hide differing underlying types behind a single type.

A brief overview and history of type erasure in Swift

This is a brief and incomplete overview of type erasure in Swift. I will be glossing over many details in order to establish a rudimentary understanding of core concepts for the purposes of this post.

Before SE-0309 (see also: SE-0353 and SE-0346), the only option was to write your own type-erased wrapper types. Even if you have never manually implemented type erasure in your own projects, you have used it. The Swift Standard Library has a number of type-erased types. One of the most common is probably AnyHashable because of its use with Dictionary. There’s also AnyCollection, AnySequence, AnyIndex, and many more. If you’re curious how these are implemented, you can view the Standard Library source code.

The convention is to prefix these wrapper types with “Any”. The strategy for implementing them is essentially copying all properties and functions from the generic type into the “Any” type, or simply storing an Any property and forwarding all members of the wrapper type to the underlying type.

Let’s use AnyHashable as a quick example. Below is a simplified implementation to demonstrate core concepts. Note that the actual implementation is significantly more complex.

struct AnyHashable: Hashable {
    var base: Any

    init<H: Hashable>(_ base: H) {
        self.base = base
    }
}

Notice that the initializer is generic. It receives a concrete type, H, that conforms to Hashable. However, it stores the base property as Any, thus erasing the concrete type. Importantly, AnyHashable also conforms to Hashable. The purpose of this is to forward the Hashable implementation to the underlying property stored in base.

But how do we forward the Hashable implementation to base if it is declared as Any? If type erasure can be described as “boxing” up a type, then we need to “unbox” the type to transform it back to its concrete type. This process of opening an Any or any type is known as reification. In Swift, we do this using the as? operator.

Below is a simplified example of implementing Hashable, which includes the hash(into:) and == functions. These calls are forwarded to the underlying base property.

struct AnyHashable: Hashable {
    var base: Any

    init<H: Hashable>(_ base: H) {
        self.base = base
    }

    // Implement Hashable

    func hash(into hasher: inout Hasher) {
        guard let base = self.base as? (any Hashable) else {
            fatalError("base should be hashable")
        }

        hasher.combine(base)
    }

    static func == (left: Self, right: Self) -> Bool {
        guard let leftBase = left.base as? (any Equatable),
              let rightBase = right.base as? (any Equatable) else {
            return false
        }

        return _isEqual(lhs: leftBase, rhs: rightBase)
    }

    private static func _isEqual<T: Equatable, U: Equatable>(lhs: T, rhs: U) -> Bool {
        if let rhsAsT = rhs as? T {
            return lhs == rhsAsT
        }

        if let lhsAsU = lhs as? U {
            return lhsAsU == rhs
        }

        return false
    }
}

Equality is especially tricky. Notice that we have to attempt to cast each side to the same concrete type before continuing with the comparison. This is because the == operator operates on two values of the same type. Otherwise, == would not make any sense — for example, attempting to compare an Int and a String is invalid.

Using AnyHashable allows us to store heterogenous types in a homogenous way. For example, Array can only store a collection of values of a single type. The following produces a compiler error:

let array = ["string", 42, 3.14, false]

However, we could instead store these values as AnyHashable, which works:

let array = [AnyHashable("string"), AnyHashable(42), AnyHashable(3.14), AnyHashable(false)]

Since Swift 5.7 and SE-0309: Unlock existentials for all protocols, type erasure is now a feature of the Swift compiler. This proposal introduced the any keyword, allowing you to write any MyProtocol to erase the generic constraints of MyProtocol. You no longer need to manually write these type-erased wrappers in most scenarios. Continuing with the AnyHashable example, you could instead write any Hashable and eliminate the need for the custom wrapper type.

However, while using any Hashable versus AnyHashable may seem equivalent, there are some important distinctions. Using any Hashable gives you an existential type, an abstract representation, while AnyHashable is a concrete type. We can observe the difference in behavior by trying to compare values of each type.

let one: any Hashable = 1
let two: any Hashable = 2

one == two // Error: Binary operator '==' cannot be applied to two 'any Hashable' operands

Attempting to compare any two values of type any <Some Protocol> will always produce an error. As mentioned above, this is because the == operator compares two values of the same (concrete) type. At compile time, it is not known what is the actual type of any <Some Protocol>, which is abstract — multiple concrete types could implement the protocol and different types cannot be Equatable, for example comparing a String and an Int does not make sense.

On the other hand, comparison of AnyHashable values works because it is a concrete type. That is, the == operator works given two AnyHashable values.

let one: AnyHashable = 1
let two: AnyHashable = 2

one == two // this works, returns false

Doug Gregor wrote an excellent post on type erasure in Swift as part of his “Swift for C++ Practitioners” series. While it is aimed at explaining Swift to C++ developers, it covers Swift in detail and I highly recommend reading it for a deeper dive. I also recommend reading this post on type erasure from Bruno Rocha.

Generics and type erasure in ReactiveCollectionsKit

Now let’s dive into the details of ReactiveCollectionsKit to see a real world example of the challenges presented by generic programming and how we can use type erasure to solve them. If you need a refresher on ReactiveCollectionsKit, you review the other posts in this series.

The core component in ReactiveCollectionsKit is the CellViewModel, which represents a cell in a collection view. Clients can provide any type they want to the library, as long as it conforms to the CellViewModel protocol. Here’s a simplified definition of CellViewModel for the purposes of this post. (You can find the full source on GitHub.) For folks familiar with the library, SupplementaryViewModel follows a similar design.

protocol CellViewModel: DiffableViewModel {
    associatedtype CellType: UICollectionViewCell

    var shouldHighlight: Bool { get }

    func configure(cell: CellType)

    func didSelect()
}

protocol DiffableViewModel: Identifiable, Hashable {
    var id: AnyHashable { get }
}

Note that CellViewModel inherits from DiffableViewModel, which powers how diffing works in the library. Also notice that CellViewModel is generic on the type of cell this view model configures. This is important — it allows clients to mix-and-match the types of cells they display in a collection and provides an ergonomic, type-safe API rather than having to cast from UICollectionViewCell to their specific cell subclass.

Here’s an example concrete implementation of a CellViewModel.

struct PersonCellViewModel: CellViewModel {
    let person: PersonModel

    // MARK: CellViewModel

    var id: UniqueIdentifier { self.person.id }

    var shouldHighlight = true

    func configure(cell: PersonCell) {
        cell.title = self.person.name
        // additional configuration
    }

    func didSelect() {
        // handle selection
    }
}

All cells in a collection view belong to a section. In ReactiveCollectionsKit, sections are modeled by SectionViewModel. This is where the challenges arise from the associatedtype in CellViewModel.

The interface we want for SectionViewModel would look something like the following. Again, this definition is simplified for the purposes of this post. Note that sections are also diffable, thus SectionViewModel also inherits from DiffableViewModel.

struct SectionViewModel: DiffableViewModel {
    let cells: [CellViewModel] // Error due to generic constraints via associatedtype
}

This does not work, because of the generic constraints in CellViewModel. To solve this, we could make SectionViewModel generic on the type of cell:

struct SectionViewModel<Cell: CellViewModel>: DiffableViewModel {
    let cells: [Cell]
}

However, this defeats the entire purpose as it restricts a section to displaying a single type of cell. The solution offered by the compiler is to use any.

struct SectionViewModel: DiffableViewModel {
    let cells: [any CellViewModel]
}

This almost works. In many contexts, you could stop here. However, this presents a new problem for ReactiveCollectionsKit. Sections and cells are diffable via DiffableViewModel, which inherits from Equatable and Hashable. Without being equatable and hashable, we have no mechanism to perform diffs on cells or sections.

As mentioned above, values with the type any <Some Protocol> are not Equatable — and thus, not Hashable, which inherits from Equatable. The problem here is that any CellViewModel is not Equatable. This is why we need to introduce a new type-erased wrapper for CellViewModel. We’ll call it AnyCellViewModel. This results in the following interface for SectionViewModel:

struct SectionViewModel: DiffableViewModel {
    let cells: [AnyCellViewModel]
}

Let’s write an initial implementation of AnyCellViewModel.

struct AnyCellViewModel: CellViewModel {
    // MARK: DiffableViewModel

    var id: AnyHashable { self._id }

    // MARK: CellViewModel

    typealias CellType = UICollectionViewCell

    var shouldHighlight: Bool { self._shouldHighlight }

    func configure(cell: UICollectionViewCell) {
        self._configure(cell)
    }

    func didSelect() {
        self._didSelect(coordinator)
    }

    // MARK: Private

    private let _id: AnyHashable
    private let _shouldHighlight: Bool
    private let _configure: (CellType) -> Void
    private let _didSelect: () -> Void

    // MARK: Init

    init<T: CellViewModel>(_ viewModel: T) {
        self._id = viewModel.id
        self._shouldHighlight = viewModel.shouldHighlight
        self._configure = {
            viewModel.configure(cell: $0 as! T.CellType)
        }
        self._didSelect = {
            viewModel.didSelect()
        }
    }
}

The strategy here is to copy all properties and methods from the CellViewModel protocol. Properties are copied directly and methods are copied as closure properties. Next, AnyCellViewModel implements the CellViewModel protocol and forwards all implementations to the copied properties. Notice we have to force-cast the cell type in the _configure() closure from the generic UICollectionViewCell from AnyCellViewModel to the specific cell subclass provided by the concrete CellViewModel we receive in the initializer. This might seem dangerous, but we know this is a safe operation.

This works as far as it satisfies the type system. However, there is more to do. The above definition of AnyCellViewModel will not work for diffing. We need to implement Equatable and Hashable, and we need more information from the original viewModel instance to do it. So far, AnyCellViewModel only knows about the members of CellViewModel. Yet, clients can provide any type that conforms to CellViewModel to the library. Consider the example above using PersonCellViewModel:

struct PersonCellViewModel: CellViewModel {
    let person: PersonModel

    // ... implementation continues ...
}

In this situation, being Equatable and Hashable involves comparing the value stored in let person: PersonModel. We cannot store a PersonModel property on AnyCellViewModel — that does not generalize. We also cannot make AnyCellViewModel generic, which defeats the entire purpose of type erasure. What we need to do is store the entire PersonCellViewModel inside AnyCellViewModel in a way that is generalized (though not using generics), but also allows us access Equatable and Hashable. That is, we cannot simply use Any.

What we can do is store the concrete view model as an AnyHashable value:

struct AnyCellViewModel: CellViewModel {
    // ... implementation continues ...

    private let _viewModel: AnyHashable

    init<T: CellViewModel>(_ viewModel: T) {
        self._viewModel = viewModel

        // ... implementation continues ...
    }
}

This erases the generic constraints of viewModel imposed by CellViewModel while preserving access to Equatable and Hashable. Now, we can simply forward the implementations from these two protocols:

extension AnyCellViewModel: Equatable {
    static func == (left: AnyCellViewModel, right: AnyCellViewModel) -> Bool {
        left._viewModel == right._viewModel
    }
}

extension AnyCellViewModel: Hashable {
    func hash(into hasher: inout Hasher) {
        self._viewModel.hash(into: &hasher)
    }
}

This resolves all of our issues. Clients receive an ergonomic, type-safe, generic API for CellViewModel. The library can successfully type erase all the values provided as AnyCellViewModel. And diffing just works in any and every scenario using Equatable and Hashable.

We can provide a convenience method to facilitate type erasure:

extension CellViewModel {
    func eraseToAnyViewModel() -> AnyCellViewModel {
        // prevent "double erasure"
        if let erasedViewModel = self as? AnyCellViewModel {
            return erasedViewModel
        }
        return AnyCellViewModel(self)
    }
}

Additionally, you’ll notice that SectionViewModel provides a number of convenience initializers using generics. In scenarios where you do not have mixed data types, the generic initializers allow you ignore this implementation detail and the library handles the type erasure for you. Below is a simplified example of one convenience initializer for SectionViewModel.

extension SectionViewModel {
    init<Cell: CellViewModel>(cells: [Cell]) {
        self.cells = cells.map { $0.eraseToAnyViewModel() }
    }
}

This initializer receives a single concrete type, Cell, that conforms to CellViewModel. It performs the type erasure internally. This greatly improves the usability of the API and helps reduce the burden on clients of having to always convert to AnyCellViewModel.

Conclusion

While generic programming is a powerful tool, there are often times where you may find yourself fighting with the type system. If your solution requires the use of generics but you are struggling to reconcile heterogenous types with generic constraints, a great tool to reach for is type erasure via any. If that does not work, like in the case of ReactiveCollectionsKit, you can write your own type-erased wrapper type. Importantly, if you need to preserve conformance to Equatable and Hashable, you can utilize AnyHashable as demonstrated above.