In Chapter 3, Types and Type Casting we talked about Abstract versus Concrete types. Type erasure is a process to make abstract types such as Generics concrete.
Why we would want to do it? The answer is because we want to write code against contracts; in other words, we want to prefer composition over inheritance. Also, sometimes we would want to be more flexible with the types. This concept may sound complicated, so let's continue our example from the previous section to understand why and how we would create type-erased structures.
First, we want to examine if we can use CustomView type and create an array with CustomView elements. We will create a CustomView:
struct CustomDisabledButton: CustomView {
typealias ViewType = Button
func configure(view with: ViewType) {
// configure the view
print("Disabled button")
}
}
So far, we've created two CustomViews; the first one represents a custom enabled button and the second one represents a disabled button.
As we have two structs with the type of CustomView, we should be able to create an array with them:
let customViews = [CustomEnabledButton(), CustomDisabledButton()]
But the compiler complains that Heterogeneous collection literal could only be inferred to '[Any]'; add explicit type annotation if this is intentional. This means that the compiler does not see them as the same type.
Maybe type annotation can help us:
let customViews: [CustomView] = [CustomSwitch(), CustomEnabledButton(),
CustomDisabledButton()]
This time, the compiler complains that Protocol 'CustomView' can only be used as a generic constraint because it has Self or associated type requirements.
This error message is misleading because we cannot specialize an associated type using Generic syntax and protocols cannot be Generic.
Type erasure is the solution to our problem. Let's create a type-erased AnyCustomView:
struct AnyCustomView<ViewType>: CustomView {
private let _configure: (ViewType) -> Void
init<Base: CustomView>(_ base: Base) where ViewType ==
Base.ViewType {
_configure = base.configure
}
func configure(view with: ViewType) {
_configure(with)
}
}
AnyCustomView is the type-erased version of CustomView and we can use it as a type when we create an array:
let views = [AnyCustomView(CustomEnabledButton()), AnyCustomView(CustomDisabledButton())]
let button = Button()
for view in views {
view.configure(view: button)
print(view)
}
This time, the compiler does not complain and prints the following:
Enabled button
AnyCustomView<Button>(_configure: (Function))
Disabled button
AnyCustomView<Button>(_configure: (Function))
This is what we needed. Let's see if we can trick the compiler to add a CustomView with a different ViewType into an array:
struct Switch {}
struct CustomSwitch: CustomView {
typealias ViewType = Switch
func configure(view with: ViewType) {
// configure the view
print("Custom switch")
}
}
let views = [AnyCustomView(CustomSwitch()),
AnyCustomView(CustomDisabledButton())]
Again, the compiler will complain that error: heterogeneous collection literal could only be inferred to '[Any]'; add explicit type annotation if this is intentional, and that is desirable because we tried to add a custom switch and a custom button to the same array. In Swift, array elements should have the same type, so we should not be able to do that. As seen from the preceding example, we were able to be more flexible with our type while we did not sacrifice type safety.
Type erasure empowers us to convert a protocol with associated types into a Generic type. Therefore, we will be able to use them as types when we define properties or parameters of functions, or when we return values. In fact, in any case that we cannot use protocols directly we will be able to use type-erased versions.