Type erasure in Swift and when it would be used

Uplift iOS Interview

The Guide is for YOU if
  • You are preparing for an iOS interview and want to improve your skills and knowledge and looking to level up your interview game and land your dream job.
  • You want to gain confidence and ease during iOS interviews by learning expert tips and curated strategies.
  • You want access to a comprehensive list of iOS interview QA to practice and prepare.

In Swift, type erasure is a technique used to abstract away the concrete type of a value and replace it with a protocol that describes the behavior of the value. This allows you to work with values in a generic way, without having to know the specific types of those values at compile time.

Type erasure is often used when you have a collection of values with different concrete types that all share a common behavior. By creating a protocol that describes that behavior and implementing it for each concrete type, you can create a collection of values that all conform to the protocol. You can then use the protocol to work with the values in the collection in a generic way, without having to know their concrete types.

Here’s an example. Let’s say you have a collection of shapes:

enum Shape {
    case circle(radius: Double)
    case rectangle(width: Double, height: Double)
    case triangle(base: Double, height: Double)
}

You want to calculate the area of each shape in the collection, but you don’t want to write separate code for each shape type. Instead, you can create a protocol that describes the behavior of shapes that have an area:

protocol HasArea {
    var area: Double { get }
}

You can then implement the protocol for each concrete shape type:

extension Shape: HasArea {
    var area: Double {
        switch self {
        case .circle(let radius):
            return Double.pi * radius * radius
        case .rectangle(let width, let height):
            return width * height
        case .triangle(let base, let height):
            return 0.5 * base * height
        }
    }
}

Finally, you can use type erasure to create a collection of values that all conform to the HasArea protocol:

struct AnyHasArea {
    var area: Double

    init<T: HasArea>(_ hasArea: T) {
        area = hasArea.area
    }
}

let shapes: [AnyHasArea] = [
    AnyHasArea(Shape.circle(radius: 1.0)),
    AnyHasArea(Shape.rectangle(width: 2.0, height: 3.0)),
    AnyHasArea(Shape.triangle(base: 4.0, height: 5.0))
]

for shape in shapes {
    print(shape.area)
}

In this example, the AnyHasArea struct uses type erasure to wrap any value that conforms to the HasArea protocol, regardless of its concrete type. This allows you to create a collection of values with different concrete types, but that all share the common behavior of having an area. You can then work with the values in the collection in a generic way, without having to know their concrete types.

Let’s take a look at another example:

protocol Vehicle {
    func start()
}

class Car: Vehicle {
    func start() {
        print("Starting car...")
    }
}

class Motorcycle: Vehicle {
    func start() {
        print("Starting motorcycle...")
    }
}

func startVehicle<T: Vehicle>(_ vehicle: T) {
    vehicle.start()
}

let car = Car()
startVehicle(car)

let motorcycle = Motorcycle()
startVehicle(motorcycle)

In this example, we have a Vehicle protocol and two classes that conform to it: Car and Motorcycle. We also have a generic function startVehicle that takes any type that conforms to Vehicle and calls its start method.

Now, suppose we want to create a collection of Vehicle objects and iterate over them, calling their start method. We might try to do this:

let vehicles: [Vehicle] = [car, motorcycle]
for vehicle in vehicles {
    startVehicle(vehicle)
}

However, this will result in a compiler error: “Protocol ‘Vehicle’ can only be used as a generic constraint because it has Self or associated type requirements”. This is because the startVehicle function is generic and expects a specific type, not a protocol with associated types.

To solve this problem, we can use type erasure to create a wrapper around the Vehicle protocol that provides a common interface. Here’s one way to do this:

class AnyVehicle {
    private let _start: () -> Void
    
    init<T: Vehicle>(_ vehicle: T) {
        _start = vehicle.start
    }
    
    func start() {
        _start()
    }
}

let anyVehicles: [AnyVehicle] = [AnyVehicle(car), AnyVehicle(motorcycle)]
for vehicle in anyVehicles {
    vehicle.start()
}

In this example, we’ve created an AnyVehicle class that takes a generic type T that conforms to Vehicle. The AnyVehicle class stores a closure that captures the start method of the underlying object. We can then create an array of AnyVehicle objects and iterate over them, calling their start method. The specific type of each object is erased, allowing them to be treated as a single type.



✍️ Written by Ishtiak Ahmed

👉 Follow me on XLinkedIn



Get Ready to Shine: Mastering the iOS Interview




Enjoying the articles? Get the inside scoop by subscribing to my newsletter.

Get access to exclusive iOS development tips, tricks, and insights when you subscribe to my newsletter. You'll also receive links to new articles, app development ideas, and an interview preparation mini book.

If you know someone who would benefit from reading this article, please share it with them.