Bad practice: not using Phantom Types
You’re more of a video kind of person? I’ve got you covered! Here’s a video with the same content than this article 🍿
Can you guess what’s the problem with this code?
I’ve defined a struct
called Person
, inside which I store a name
and an id
.
You can see that I’m using a UUID
to represent the id
, which guarantees uniqueness.
Now let’s introduce a second struct
, called Location
.
I’m following the same approach and I also use a UUID
to represent the id
of that new struct
.
For now, this code seems pretty reasonable, doesn’t it?
Now let’s introduce a function that operates over an Array
of Location
.
You can notice that there is nothing preventing me from filtering the array of Location
using the id of a Person
.
And that’s a problem, because such code doesn’t make any sense and is guaranteed to lead to a bug!
However, there’s an easy way to improve!
We can introduce a new struct called ID
.
In this struct
, we’ll store a UUID
.
And we’re also going to add a generic argument to this struct
.
This might seem weird, because we’re not using this generic argument anywhere, but trust me: it will all make sense in a second!
Because now, we’re going to remove the UUID
from both Person
and Location
…
…and replace them with our new ID
type.
If we look at the type of each id
property, we can see that, thanks to the extra generic argument, the types are no longer the same!
This means that if by mistake we attempt to compare the id
of a Person
with the id
of a Location
, we’ll get a compile-time error and it will be impossible to ship this incorrect code!
That’s all for this article, I hope you’ve enjoyed learning this new trick!
Here’s the code if you want to experiment with it:
import Foundation
struct ID<T>: Equatable {
private let value = UUID()
}
struct Person {
let id = ID<Self>() // `id` is of type `ID<Person>`
let name: String
}
struct Location {
let id = ID<Self>() // `id` is of type `ID<Location>`
let coordinates: (Double, Double)
}
func handle(locations: [Location]) {
let me = Person(name: "Vincent")
let filtered = locations.filter { $0.id == me.id } // compile-time error
}