Type erasure in Swift can be explained as the process of enforcing type constraints only at compile time and discarding the element type information at runtime. It is often used when working with protocols that have associated types or use the Self
metatype, which makes them impossible to reference as stand-alone protocols.
The goal is to hide the specific type behind a more abstract type that can be used in containers or functions, simplify your code and improve the interoperability of your types, useful for various use cases, such as:
Self
requirements in contexts where the concrete type is not known or relevant.Some examples of built-in type erased types in Swift, often starting with Any, are: AnyHashable
, AnySequence
, AnyPublisher
, AnyView
, or AnyCancellable
.
Using a wrapper type is a common way to implement type erasure by creating a generic type that conforms to the protocol and wraps an instance of any type that also conforms to the protocol.
Here’re an example to create type erased AnyShape
:
// The protocol with an associated type
protocol Shape {
associatedtype Area
var name: String { get }
var area: Area { get }
}
// A struct that conforms to the Shape protocol
struct Square: Shape {
var side: Double
var name: String { "Square" }
var area: Double { side * side }
}
// A struct that conforms to the Shape protocol
struct Circle: Shape {
var radius: Double
var name: String { "Circle" }
var area: Double { .pi * radius * radius }
}
// The type erased wrapper
struct AnyShape<Area>: Shape {
private let nameClosure: () -> String
private let areaClosure: () -> Area
init<T: Shape>(_ shape: T) where T.Area == Area {
nameClosure = { shape.name }
areaClosure = { shape.area }
}
var name: String {
nameClosure()
}
var area: Area {
areaClosure()
}
}
// This will compile
let shapes: [AnyShape<Double>] = [
AnyShape(Square(side: 10)),
AnyShape(Circle(radius: 5))
]
Types that are created by using custom type-erased wrappers can be rewritten using the any
keyword since Swift 5.7 in most cases (but not all).
// This will compile
let shapes: [any Shape] = [
Square(side: 10),
Circle(radius: 5)
]
However, there are some cases where you cannot use the any
keyword to replace custom type-erased types if they add additional functionality or requirements that are not part of the protocol it conforms to. For example, an AnyCancellable
instance automatically calls cancel()
when deinitialized.