Exploring Type-Safe Identifiers in Swift

Using the Identifiable protocol

Yury Buslovsky
Better Programming

--

Note that this implementation uses Identifiable protocol which is only available on iOS 13+, so you might try to use a “stripped” version without this particular conformance for older systems.

The problem

In simple words, an identifier is a sequence of characters, unique to a model of one certain type.

For example, if there is a user model in your application, and it has an ID property, then it is guaranteed that its value will never occur more than once in a set of user models.

Let’s pretend we are developing a social network app where users can write posts and leave comments. Their hypothetical models would be:

Imagine that, for some reason, on one hand, users have String identifiers, and on the other, posts and comments use integer IDs. Once again — no problem with users, their IDs are known to be unique.

However, when it comes to other models, there are some pitfalls. Not only can some post’s ID be equal to some comment’s one, we also are actually able to compare them by accident. And the biggest issue is that the compiler cannot help us whatsoever.

The desirable outcome is when it is only possible to compare compatible IDs:

Let’s try to implement this behavior!

Phantom types

Generics are truly powerful in Swift. One of their many applications is the ability to use them as phantom types. Simply speaking, a type is called phantom when it is declared but not utilized by any members.

It is only needed for compiler to distinguish certain container types. The simplest implementation of such container for an ID would be:

A little bit cumbersome, don’t you think? The declaration and the initialization of those IDs are too verbose.

We even have to introduce a dummy enum for each model just to use an identifier. Let’s try and encapsulate the implementation!

Improved solution

First, nested type aliases for ID types should be used, so that we don’t have to know a model’s ID type outside its scope.

I would want to just write something like let post: Post.ID without worrying about what is used behind the scenes.

Second, maybe there is a way to get rid of dummy enums? Perhaps :)

Third, let’s simplify the initialization of identifiers. It’s feasible due to the ExpressibleBy... protocols group.

Bonus: we can make our models compatible with SwiftUI iteration mechanisms, such as ForEach, List, etc., out of the box just by incorporating Identifiable protocol.

Given the aforementioned points, here goes the updated implementation:

Now, let’s enhance this solution with an ability to be expressed by literals of Swift’s fundamental types:

As a result, we can declare our models like the following:

Eventually, our models can be used like this:

Conclusion

Neat! This implementation can be improved with Decodable and whatever else you might find yourself needing, but that is out of the scope of this article.

Thanks for reading!

--

--