How to use type erasure in Swift with a real example

Unlocking flexibility and reusability by mastering type erasure in Swift

Bruno Lorenzo
6 min readJul 25, 2023
Photo by Nubelson Fernandes on Unsplash

One of the remarkable attributes of Swift is its strong focus on type safety, which ensures a robust and reliable codebase.

However, we might find ourselves in situations where we need a little more flexibility. We might want to hide or abstract a specific value type to make our code more generic and reusable. We want to type erasure.

This is a powerful technique that, as an iOS developer, you should master. Because, sooner or later, you will need to use it.

Let’s jump into a practical example to see how it works.

Our coffee shop app allows our customers to add coffee drinks and food to their orders. As we have different sizes for the coffees than for the food, we could model our entities as follow:

struct Coffee {
enum CoffeeType {
case latte
case cappuccino
case mocha
}

enum CoffeeSize {
case small
case medium
case large
}

let type: CoffeeType
let size: CoffeeSize

private let prices: [CoffeeType: [CoffeeSize: Float]] = [
.latte: [
.small: 4.5,
.medium: 5.5,
.large: 6.0
],
.cappuccino: [
.small: 4.5,
.medium: 5.5,
.large: 6.0
],
.mocha: [
.small: 4.5,
.medium: 5.5,
.large: 6.0
]
]

var price: Float {
return prices[type]?[size] ?? 0.0
}
}

struct Food {
enum FoodType {
case croissant
case muffin
case sandwich
}

enum FoodSize {
case small
case regular
}

let type: FoodType
let size: FoodSize

private let prices: [FoodType: [FoodSize: Float]] = [
.croissant: [
.small: 8,
.regular: 12,
],
.muffin: [
.small: 6.5,
.regular: 8,
],
.sandwich: [
.regular: 8.5
]
]

var price: Float {
return prices[type]?[size] ?? 0.0
}
}

The Problem

With this approach, we must hold two different lists for our menu to build our view.

  • One for the coffee
  • One for the food
let coffeeMenu: [Coffee] = [
Coffee(type: .latte, size: .small),
Coffee(type: .latte, size: .regular),
Coffee(type: .latte, size: .large),
Coffee(type: .cappuccino, size: .small),
Coffee(type: .cappuccino, size: .regular),
Coffee(type: .cappuccino, size: .large),
Coffee(type: .mocha, size: .small),
Coffee(type: .mocha, size: .regular),
Coffee(type: .mocha, size: .large),
]

let foodMenu: [Food] = [
Food(type: .croissant, size: .small),
Food(type: .croissant, size: .regular),
Food(type: .muffinn, size: .small),
Food(type: .muffinn, size: .regular),
Food(type: .sandwich, size: .regular)
]

struct HomeView: View {
...
var body: some View {
...
ScrollView {
ForEach(coffeeMenu, id: \.self) { cofee in
// Configure UI for coffee
}
ForEach(foodMenu, id: \.self) { food in
// Configure UI for food
}
}
}

If we pay attention, the UI for both coffee and food is pretty similar. The main difference is when we tap on one item and show each size.

How do we adjust it?

Using protocols and generics.

This is where type erasure comes into place.

The main idea is to define a common interface so that both Coffee and Food structures implement it. Then we can create only one list to define our menu and use it in our view.

protocol MenuItem {
associatedtype ItemSize
func price(for size: ItemSize) -> Float
}

extension Coffee: MenuItem {
enum ItemSize {
case small
case medium
case large
}

func price(for size: ItemSize) -> Float {
return prices[type]?[size] ?? 0
}

var prices: [CoffeeType: [ItemSize: Float]] {
return [
.latte: [
.small: 4.5,
.medium: 5.5,
.large: 6.0
],
.cappuccino: [
.small: 4.5,
.medium: 5.5,
.large: 6.0
],
.mocha: [
.small: 4.5,
.medium: 5.5,
.large: 6.0
]
]
}
}

extension Food: MenuItem {
enum ItemSize {
case small
case regular
}

func price(for size: ItemSize) -> Float {
prices[type]?[size] ?? 0
}

private var prices: [FoodType: [ItemSize: Float]] {
return [
.croissant: [
.small: 8,
.regular: 12,
],
.muffin: [
.small: 6.5,
.regular: 8,
],
.sandwich: [
.regular: 8.5
]
]
}
}

let menu: [MenuItem] = [
Coffee(type: .latte),
Coffee(type: .cappuccino),
Coffee(type: .mocha),
Food(type: .croissant),
Food(type: .muffin),
Food(type: .sandwich),
]

If we try to execute the above code, we’re going to get an error from the compiler. This happens because the compiler does not know what type ItemSize is.

Even though the compiler will know for sure the type in runtime, it can’t be resolved in compile time. As Swift is meant to be type-safe, we get the error.

How do we solve it?

Creating a wrapper that hides MenuItem implementations.

struct AnyMenuItem {
private let _description: String
private let _sizes: [Any]
private let _price: (Any) -> Float

init<T: MenuItem>(_ menuItem: T) {
_description = menuItem.description
_sizes = menuItem.sizes as [Any]
_price = { size in
guard let itemSize = size as? T.ItemSize else {
fatalError("Invalid item size")
}
return menuItem.price(for: itemSize)
}
}

var description: String {
return _description
}

var sizes: [Any] {
return _sizes
}

func price(for size: Any) -> Float {
return _price(size)
}
}

let menu: [AnyMenuItem] = [
AnyMenuItem(Coffee(type: .latte)),
AnyMenuItem(Coffee(type: .mocha)),
AnyMenuItem(Food(type: .croissant)),
AnyMenuItem(Food(type: .muffin)),
]

for item in menu {
print("Prices for \(item.description):")
for size in item.sizes {
print("- \(size): $\(item.price(for: size))")
}
print("----------------------------")
}

// Output👇🏻
Prices for Latte:
- small: $4.5
- medium: $5.5
- large: $6.0
----------------------------
Prices for Mocha:
- small: $4.5
- medium: $5.5
----------------------------
Prices for Croissant:
- small: $8.0
- regular: $12.0
----------------------------
Prices for Muffin:
- regular: $8.0
----------------------------

This is what it’s called type erasure. By using AnyMenuItem we're hiding the real type that we want to use. In our case: Coffee and Food.

With this approach, we gain on abstraction, but we have a big downside: the use of Any in AnyMenuItem.

Consider the following code:

let menu: [AnyMenuItem] = [
AnyMenuItem(Coffee(type: .latte)),
AnyMenuItem(Food(type: .muffin)),
]

for item in menu {
print(item.price(for: Coffee.ItemSize.small))
}

The code will compile just fine and we won’t get any errors from the compiler. However, if we execute, we’ll get a crash: Invalid item size.

Why? Because Coffee.ItemSize.small is not compatible with Food.ItemSize.

Lucky us

From Swift 5.7 we can use the new keyword any to make our type erasure process type-safe. Using it can avoid the “manual” erasure we did with AnyMenuItem struct.

let menu: [any MenuItem] = [
Coffee(type: .latte),
Coffee(type: .mocha),
Food(type: .croissant),
Food(type: .muffin),
]

for item in menu {
print("Prices for \(item.description)")
for pricelist in getPriceList(for: item) {
print("- \(pricelist.size): $\(pricelist.price)")
}
}

func getPriceList<T: MenuItem>(for item: T) -> [(size: T.ItemSize, price: Float)] {
var prices: [(T.ItemSize, Float)] = []
for size in item.sizes {
prices.append((size: size, price: item.price(for: size)))
}
return prices
}

// Output👇🏻
Prices for Latte
- small: $4.5
- medium: $5.5
- large: $6.0
Prices for Mocha
- small: $4.5
- medium: $5.5
Prices for Croissant
- small: $8.0
- regular: $12.0
Prices for Muffin
- regular: $8.0

Yep, that’s it.

Notice: If we attempt to call item.price(for: size) directly, we will get an error. Apparently, the compiler needs to be sure of the type that we're using (ItemSize in our case). That's why we need to use a generic function (getPriceList<T: MenuItem>(for:T)) to fulfill the necessary type constraints.

Takeaways

  • We can create reusable code that is decoupled from specific types, making it easier to maintain and extend.
  • Hide the specific type of a value and work with it in a more generic and flexible manner.
  • If you’re using Swift 5.7 o higher, take advantage of the built-in type erasure that Swift provides us by using anyinstead of creating a wrapper.
  • Type erasure is not a fit for every case, you should carefully evaluate if it's the right technique to use.

Have any questions? Feel free to drop me a message! 🙂

  • 🤓 Join me on Twitter for regular content on iOS development tips and insights
  • 🚀 Check out my GitHub where I share all my example projects

--

--

Bruno Lorenzo
Bruno Lorenzo

Written by Bruno Lorenzo

Software Engineer | Innovation Manager at www.houlak.com | Former iOS Tech Lead | I write about iOS, tech, and producitivy

Responses (1)