Custom Identifier vs Identifiable.ID

While Identifiable.ID is a valid approach, it can still cause unexpected behaviour. Let's find an easy workaround by creating a small, generic type.

The Identifiable protocol has been introduced to the Swift Standard Library with Swift 5.1 and even before has been a part of SwiftUI and is commonly used with all our data models since then. And some of you may even used some similar protocols created by the community or your own. I still prefer a custom Identifier type to Identifiable.ID and I want to show you why.

For this example I'll use two simple models:

struct Author: Identifiable {
  let id: String
  let name: String
}

struct Post: Identifiable {
  let id: String
  let title: String
  let content: String
  let author: Author
}

Now let's imagine a function that'll grab all posts of an author:

func getAllPosts(by authorId: Author.ID) -> [Post]

With this definition, the following code will compile without issues:

let post: Post // Currently viewed post

getAllPosts(by: post.author.id) // this will return the desired result
getAllPosts(by: post.id) // This will return unexpected results

This works, because Author.ID and Post.ID in this example are both of the type String and the .ID is only a typealias and not a separate type itself. If the author ID would've been an Int, the compiler would've thrown an error, because the types don't match.

But lets solve this with a custom Identifier object.

Custom Identifier

No, I don't want to create a separate object for each main model. Generics will help us here.

struct Identifier<T: Identifiable> {
  let rawValue: Value.ID
  init(rawValue: Value.ID) {
    self.rawValue = rawValue
  }
}

and a little extension to all Identifiable objects:

extension Identifiable {
  var identifier: Identifier<Self> {
    return .init(rawValue: self.id)
  }
}

What we can do now is update our function from above:

// Old: func getAllPosts(by authorId: Author.ID) -> [Post]
func getAllPosts(by authorId: Identifier<Author>) -> [Post]

And now change the usage of the code:

let post: Post // Currently viewed post

getAllPosts(by: post.author.identifier) // this is fine
getAllPosts(by: post.author.id) // this will cause a compile error
getAllPosts(by: post.identifier) // this will cause a compile error

To get easy access to the Identifier of a model, we use the extension to Identifiable from above. The regular .id won't work here as this will keep returning the String value instead of the desired Identifier<Author>.

With this, you get much safer code in the same way Swift itself prevents us humans (and AI) from doing to many mistakes that end up in unexpected results and unnecessary debugging sessions.