Swift is a type-safe, statically typed language, meaning that each value has a type and the compiler performs type checking at compile time. Your code won't compile if it contains type errors. The benefit is that common bugs are caught early by the compiler. Swift's type system is strict and that also has its drawbacks. It makes the language less dynamic compared to other languages you may be familiar with, such as Ruby and JavaScript.

To work around the limitations of Swift's type system, it is at times necessary to hide the type of a value. Hiding or erasing the type of a value is better known as type erasure. Like dependency injection, type erasure may feel like a daunting concept at first, but it isn't that difficult to understand.

Type erasure comes in a variety of forms and it is likely that you have used type erasure in some of your projects, possibly without knowing it. In this series, you learn what type erasure is, why it is a useful pattern, and we take a look at a few examples of type erasure in Swift.

What Is Type Erasure?

As the name suggests, type erasure simply means hiding or erasing the type of a value. There are several ways to accomplish that and we take a look at several examples in this series.

As I mentioned earlier, Swift is a type-safe language. Each value has a type. Even if you use type erasure, the compiler is still able to access the type of the value whose type is erased. Type safety is a fundamental concept of the language and type erase doesn't change that. Keep that in mind.

Hiding a Type Behind a Protocol

While type erasure is a simple concept, it can complicate your code quite a bit. I want to start simple with an example that illustrates how you can hide a value's type. The example we look at isn't considered a form of type erasure, but it illustrates the concept of hiding the type of a value and why that can be useful.

Fire up Xcode and create a playground. Add an import statement for the Foundation framework at the top.

import Foundation

We declare three structs, Photo, Letter, and Printer. The Photo struct has a property, image, of type Data and the Letter struct has a property text of type String. The Printer struct can print photos and letters. It defines a print(_:) method that accepts a Photo object and it defines a print(_:) method that accepts a Letter object.

import Foundation

struct Photo {
    let image: Data
}

struct Letter {
    let text: String
}

struct Printer {
    func print(_ photo: Photo) {

    }

    func print(_ letter: Letter) {

    }
}

Let's create a Printer object and print a photo and a letter.

let printer = Printer()
printer.print(Photo(image: Data()))
printer.print(Letter(text: "My Letter"))

The implementation has a few flaws, though. To add support for printing postcards, we need to extend the Printer struct with a method that accepts a Postcard object. This may be acceptable for now, but I hope you agree that this isn't a scalable solution. The implementation also suffers from tight coupling. The Printer struct is tightly coupled to the Photo and Letter structs. Should a printer know about photos and letters? It should only know how to print them. Right?

Let's clean up the implementation with a protocol. We define a protocol with name Printable. The protocol defines a single property, data, of type Data.

protocol Printable {
    var data: Data { get }
}

The printer should be able to print a Printable object so we define a print(_:) method that accepts a Printable object. It asks the Printable object for the data that needs to be printed through the protocol's data property.

struct Printer {
    func print(_ photo: Photo) {

    }

    func print(_ letter: Letter) {

    }

    func print(_ printable: Printable) {

    }
}

We can decouple Printer from Photo and Letter by conforming Photo and Letter to Printable. The only requirement is that Photo and Letter declare a property with name data of type Data.

struct Photo: Printable {
    let image: Data

    var data: Data {
        image
    }
}

struct Letter: Printable {
    let text: String

    var data: Data {
        text.data(using: .utf8) ?? Data()
    }
}

Because Photo and Letter conform to Printable, we can remove the print(_:) methods that accept a Photo or a Letter object.

struct Printer {
    func print(_ printable: Printable) {

    }
}

The Printer object can still print photos and letters.

let printer = Printer()
printer.print(Photo(image: Data()))
printer.print(Letter(text: "My Letter"))

We created the Printable protocol to hide the type of the value that we pass to the print(_:) method. This pattern is better known as protocol-oriented-programming. While it isn't considered a form of type erasure, it clearly illustrates what hiding a value's type means and what the benefits are of doing so.

The only requirement of the print(_:) method is that the object that needs to be printed conforms to the Printable protocol. The solution we implemented promotes decoupling. The Printer struct is no longer tightly coupled to Photo and Letter. It only cares that the object that is passed to the print(_:) method conforms to the Printable protocol.

Adding support for postcards becomes trivial. We don't need to make changes to the Printer struct. We define a struct with name Postcard that conforms to the Printable protocol. The fact that we don't need to make changes to Printer to add support for postcards confirms that the Printer struct isn't tightly coupled to Postcard.

struct Postcard: Printable {
    let text: String

    var data: Data {
        text.data(using: .utf8) ?? Data()
    }
}
let printer = Printer()
printer.print(Photo(image: Data()))
printer.print(Letter(text: "My Letter"))
printer.print(Postcard(text: "My Postcard"))

What's Next?

Even though Swift's type system is less flexible than that of a loosely typed language, the language supports patterns, such as type erasure and protocol-oriented programming, that allow you to write flexible code. In the next episode, we take a look at type erasure in action and what problems it can solve.