Compiler time safety with phantom types

Swift, unlike Objective-C, tries to be a safe language. And by safe language, I mean that tries to force you to rely on compile-time safety checks, enforcing the notion that code should be as safe as possible at runtime.

So, in order to do that, Swift enforces a very rigid, even pedantic type system. Which can be certainly annoying sometimes, but, on the other hand, it can help catch potential errors at build time, by being smart and relying on the compiler.

Phantom types are types that are only declared in order to increase the compile-time safety of code.

For example. Imagine we need to write a function that assigns dishes to a meal. We can pass the meal identifier and the dish identifier to that function, and it will match both.

let dinnerRawIdentifier = "meal_45bcdf4"
let meatballsRawIdentifier = "dish_123ddc"

func assignDishToMeal(dish: String, meal: String) {
    print("dish \(dish) assigned to meal \(meal)")
}

assignDishToMeal(meatballsRawIdentifier, meal: dinnerRawIdentifier)

But if we pass those parameters in the wrong order by mistake, we won’t have any error at build time, although the code will not work as expected.

//This will compile. But it won't work as expected
assignDishToMeal(dinnerRawIdentifier, meal: meatballsRawIdentifier)

However, let’s say that we declare a Generic Identifier type.

struct Identifier<T> {
    let identifier: String
}

Also, we declare two phantom types, in this case as enums (so they can not be instantiated). We want these types only as “tags” we are never going to actually use or assign them.

enum Meal{}
enum Dish{}

So now, we can rewrite the previous function to:

func assignDishToMeal(dish: Identifier<Dish>, meal: Identifier<Meal>) {
    print("dish \(dish.identifier) assigned to meal \(meal.identifier)")
}

We declare the new Identifiers, tagged with he phantom types:

let dinnerIdentifier = Identifier<Meal>(identifier: dinnerRawIdentifier)
let meatballsIdentifier = Identifier<Dish>(identifier: meatballsRawIdentifier)

And now, the compiler will complain if we mess up:

assignDishToMeal(meatballsIdentifier, meal: dinnerIdentifier)

// The compiler will throw an error if we try to do this:
//assignDishToMeal(dinnerIdentifier, meal: dinnerIdentifier)

Leave a Reply

Your email address will not be published. Required fields are marked *