Video #12: Tagged
Episode: Video #12 Date: Apr 16, 2018 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep12-tagged

Description
We typically model our data with very general types, like strings and ints, but the values themselves are often far more specific, like emails and ids. We’ll explore how this can lead to subtle runtime bugs and how we can strengthen these types in an ergonomic way using several features new to Swift 4.1.
Video
Cloudflare Stream video ID: 94d772eb85936f0c8388ef6ac5f2b1e6 Local file: video_12_tagged.mp4 *(download with --video 12)*
References
- Discussions
- Tagged
- here
- the Point-Free code base
- models
- third-party services
- email addresses
- more
- Type-Safe File Paths with Phantom Types
- Brandon Kase
- 0012-tagged
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- MIT License
Transcript
— 0:05
We often work with types that are far too general or hold far too many values than what is necessary in our domains. In our past episode on algebraic data types we used algebra to help simplify certain types to chisel them down to their core essentials. However, sometimes that isn’t possible. Sometimes we just want to differentiate between two seemingly equivalent values at the type level. For example, an email address is nothing but a string, but it should be restricted in the ways in which it can be used. Also, ids in our code may all be integers, but it doesn’t make sense to compare a user’s id with, say, a subscription id. They are completely different entities. We want to show that Swift, and in particular Swift 4.1, gives us an incredible tool to lift types up to a higher level so that they can distinguished from each other more easily. This helps improve the contracts we are building into our functions and methods, and overall makes our code safer. Decodable
— 0:53
Let’s take a look at some sample data that we may get back from an API or other data store. We have some users and subscriptions represented in JSON. let usersJson = """ [ { "id": 1, "name": "Brandon", "email": " [email protected] ", "subscriptionId": 1 }, { "id": 2, "name": "Stephen", "email": " [email protected] ", "subscriptionId": null }, { "id": 3, "name": "Blob", "email": " [email protected] ", "subscriptionId": 1 } ] """ let subscriptionsJson = """ [ { "id": 1, "ownerId": 1 } ] """
— 1:17
And we have a couple corresponding structs with property names and types that match up. struct Subscription { let id: Int let ownerId: Int } struct User { let id: Int let name: String let email: String let subscriptionId: Int? }
— 1:32
Swift has a powerful pair of protocols, Encodable and Decodable , that will automatically synthesize encoding and decoding logic for structs that are made up of types that conform to each protocol respectively.
— 1:46
How do we make these types decodable? We can conform this Subscription type: struct Subscription: Decodable { let id: Int let ownerId: Int }
— 1:53
And everything still builds. Swift has now synthesized an initializer for this type: Subscription.init(from:) // (Decoder) throws -> Subscription
— 2:08
Alright, let’s do the same with User . struct User: Decodable { let id: Int let name: String let email: String let subscriptionId: Int? }
— 2:17
With these in place, we can use JSONDecoder to decode values from our JSON. let decoder = JSONDecoder() let users = try! decoder.decode( [User].self, from: Data(usersJson.utf8) ) let subscriptions = try! decoder.decode( [Subscription].self, from: Data(subscriptionsJson.utf8) )
— 2:59
Nice! We didn’t have to manually write any logic for decoding our data. The compiler did it for us! Our contract is written in the data types themselves, and that’s pretty powerful.
— 3:11
Our data types are kinda loose, though. We’re using Int and String because they’re decodable out of the box, but it’d be nice to strengthen them at the type level so we’re prevented from doing strange things to an id or email address.
— 3:43
Say we have a function that can send an email to an address. func sendEmail(email: String) { … }
— 3:53
Well, we can fetch a user from our store and send them an email. let user = users[0] sendEmail(email: user.email)
— 4:02
This compiles and runs, but there’s nothing stopping us from writing this: sendEmail(email: user.name) And we’re left with a runtime error waiting to happen. We’re representing email addresses as strings, but not every string is a valid email address.
— 4:15
What we want is a type that is more constrained than String . Let’s wrap our string in an Email struct and change sendEmail to only accept Email values as input. struct Email { let email: String }
— 4:31
We can now use this type to represent our user’s email. struct User: Decodable { let id: Int let name: String let email: Email let subscriptionId: Int? }
— 4:34
And we can use this type in our sendEmail function. func sendEmail(email: Email) { … }
— 4:39
Now sendEmail now has a compiler error when we try to pass a string. sendEmail(email: user.name) Cannot convert value of type ‘String’ to expected argument type ‘Email’ Swift’s now providing a stricter check for us at the type level.
— 4:46
We have another compiler error, though. Type ‘User’ does not conform to protocol ‘Decodable’ The compiler can no longer automatically conform User to Decodable because Email isn’t Decodable . That’s easy enough to fix. struct Email: Decodable { let email: String }
— 4:56
Now we’re getting a runtime error when we decode our users. Fatal error: ‘try!’ expression unexpectedly raised an error: Swift.DecodingError.typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: “Index 0”, intValue: 0), CodingKeys(stringValue: “email”, intValue: nil)], debugDescription: “Expected to decode Dictionary<String, Any> but found a string/data instead.”, underlyingError: nil))
— 5:16
When Swift generates a decoding initializer for a structs, it presumes that the data is wrapped in a container keyed by property name. This means the JSON decoder is expecting an object with an email key, not a raw string. Something like this: // "email": {"email": String}
— 5:29
We want to flatten this so that it matches the JSON we’re working with. We can write our own custom initializer to resolve the issue. Decoders have a few methods for producing decoding containers. KeyedDecodingContainer s decode values that are keyed, like our structs are keyed by property name, so this is what it appears to be using for us by default. UnkeyedDecodingContainer s are for arrays. What we want to use is the SingleValueDecodingContainer , which, true to its name, is responsible for decoding a single value. struct Email: Decodable { let email: String init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() self.email = try container.decode(String.self) } }
— 6:29
Great! Everything compiles, runs, and still restricts us from sending emails to raw strings. Having to provide that custom initializer is a bit of a bummer, though, and boilerplate we should be able to avoid. Raw representable
— 7:11
Let’s look at another useful protocol that ships with Swift: RawRepresentable . RawRepresentable is a small protocol that describes the wrapping of another value. You may be conforming types to it without even knowing it.
— 7:26
Let’s take a look at an example: enum Status: Int { case ok = 200 case notFound = 404 } It’s common to create these kinds of enums to make code more type-safe. They wrap a more general type in a more specific container, kinda like our Email struct! These enums automatically conform to RawRepresentable , providing an interface to and from its raw value. Status.ok.rawValue // 200 Status(rawValue: 200) // Status.ok Status(rawValue: 600) // nil
— 8:13
This is probably the most common use of RawRepresentable , but nothing’s stopping us from conforming manually. We can manually conform Email to RawRepresentable . We can let Swift fill in the protocol stubs. struct Email: Decodable, RawRepresentable { init?(rawValue: String) { self.email = rawValue } var rawValue: String { return self.email } typealias RawValue = String let email: String init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() self.email = try container.decode(String.self) } }
— 8:52
There’s now a lot going on here, but we can at least delete our manual Decodable initializer: RawRepresentable gives us one that does the same work for free. struct Email: Decodable, RawRepresentable { init?(rawValue: String) { self.email = rawValue } var rawValue: String { return self.email } typealias RawValue = String let email: String }
— 9:10
This is still a lot, but if we rename email to rawValue , we can shorten things substantially. struct Email: Decodable, RawRepresentable { let rawValue: String }
— 9:18
Everything still builds! We’re able to rely on the default, member-wise initializer that Swift synthesizes for us.
— 9:26
This isn’t a lot of code, but it’s generating a lot of usefulness. Merely by marking our struct Decodable , RawRepresentable , and naming its property rawValue , Swift is allowing us to create a type-safe wrapper for the boundaries of our application.
— 9:41
Things are so short that we can really make it a one-liner. struct Email: Decodable, RawRepresentable { let rawValue: String }
— 9:45
All it takes is this single line to make our application more type-safe. Our JSON didn’t change at all, but the email property is now fully constrained, and we’re not going to accidentally write code that tries to send an email to a user’s name string.
— 9:58
It’s so short that we should be able to spring these types throughout our code base for even more protection.
— 10:12
Let’s look at another potential issue. Say we want to fetch a user’s subscription. let subscription = subscriptions .first(where: { $0.id == user.subscriptionId })
— 10:37
This works, but nothing prevents us from accidentally comparing on a user’s id instead. let subscription = subscriptions .first(where: { $0.id == user.id })
— 10:47
Now we have a runtime bug just waiting to happen that may return the wrong data to a user. It’s a security issue and we should capture it at the type level.
— 11:02
Easy enough, we just need to make another RawRepresentable struct to represent a subscription’s id. struct Subscription: Decodable { struct Id: Decodable, RawRepresentable { let rawValue: Int } let id: Id let ownerId: Int }
— 11:30
We also want to make sure that User represents its subscriptionId using this type. struct User: Decodable { let id: Int let name: String let email: Email let subscriptionId: Subscription.Id? } We now have a couple compiler errors, which is to be expected. Our first, correct use of is failing because we haven’t defined equality for Subscription.Id , and our second, incorrect use is correctly failing because we don’t want to be comparing a subscription id with a user id.
— 12:07
Let’s look closer at the first error. Binary operator ‘==’ cannot be applied to operands of type ‘Subscription.Id’ and ‘Subscription.Id?’ This is kinda a subtle issue, because the following works: let subscription = subscriptions .first { $0.id == user.subscriptionId! }
— 12:18
This is because Swift ships with a generic overload on == that handles RawRepresentable values where the associated RawValue type is equatable, but no overloads for the cases where either side is optional. RawRepresentable can’t conditionally conform to Equatable because it’s a protocol, and protocol extensions can’t have inheritance clauses.
— 12:43
We can fix this by conforming Subscription.Id to Equatable . struct Subscription: Decodable { struct Id: Decodable, RawRepresentable, Equatable { let rawValue: Int } let id: Id let ownerId: Int } And we can get rid of our force-unwrap. let subscription = subscriptions .first { $0.id == user.subscriptionId! }
— 12:58
Didn’t even have to write more code! Swift was able to derive conformance for us since Int already conforms to Equatable .
— 13:19
So much power for free! We’re creating new types that wrap more primitive types and create very strong contracts in our APIs. It’s it getting a bit noisy, though, and even though Swift is writing a lot of code for us, we sure are having to tell it over and over which code it should write. There’s gotta be a better way. Tagged
— 13:54
Let’s write our own, all-purpose struct to wrap raw values. struct Tagged<Tag, RawValue> { let rawValue: RawValue }
— 14:15
We’ve called it Tagged to account for an extra Tag generic parameter. It’s a strange parameter that doesn’t appear to be used anywhere in the type. This is sometimes called a “phantom type”.
— 14:32
What can we do with this? Let’s replace our Subscription.Id struct with Tagged . struct Subscription: Decodable { typealias Id = Tagged<Subscription, Int> let id: Id let ownerId: Int } We’ve tagged this Id type with the Subscription type itself.
— 14:50
Alright, well now we’ve broken some things. User is no longer Decodable because Tagged isn’t decodable. Luckily, Swift gives us a tool to solve this: conditional conformance. Let’s conditionally conform Tagged to Decodable . extension Tagged: Decodable where RawValue: Decodable { init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() self.init(rawValue: try container.decode(RawValue.self)) } } Correction This implementation of init(from decoder:) incorrectly makes the assumption that a tagged value is always a “single value”, preventing tagged keyed and unkeyed container types (like structs, arrays, and dictionaries) from being decodable. The fix is a simpler implementation that delegates to the raw value directly and was submitted as a pull request here .
— 15:23
Great, our Subscription is Decodable again, but we have another issue. Binary operator ‘==’ cannot be applied to two ‘Subscription.Id’ (aka ‘Tagged<Subscription, Int>’) operands
— 15:27
This is because Tagged does not conform to Equatable . We’re not dealing with RawRepresentable anymore, so we no longer have the == overload. Because we’re dealing with a concrete type, we can use conditional conformance to make Tagged equatable. extension Tagged: Equatable where RawValue: Equatable { static func == (lhs: Tagged, rhs: Tagged) -> Bool { return lhs.rawValue == rhs.rawValue } } Whoa. Everything’s working again! let subscription = subscriptions .first { $0.id == user.subscriptionId } We get back the intended subscription! What about our problematic logic? let subscription = subscriptions .first { $0.id == user.id } Binary operator ‘==’ cannot be applied to operands of type ‘Subscription.Id’ (aka ‘Tagged<Subscription, Int>’) and ‘User.Id’ (aka ‘Tagged<User, Int>’)
— 16:16
Wow, Tagged solved all the same problems as RawRepresentable but in less code that in some cases seems to perform the task better.
— 16:35
Let’s try it out with the user’s id. We can define a tagged id in our User struct. struct User: Decodable { typealias Id = Tagged<User, Int> let id: Id let name: String let email: Email let subscriptionId: Subscription.Id? }
— 16:54
And update our Subscription struct accordingly. struct Subscription: Decodable { typealias Id = Tagged<Subscription, Int> let id: Id let ownerId: User.Id } And everything still compiles!
— 17:22
Now, when we try to compare a subscription id and a user id, we get a very specific, readable error message: Binary operator ‘==’ cannot be applied to operands of type ‘Subscription.Id’ (aka ‘Tagged<Subscription, Int>’) and ‘User.Id’ (aka ‘Tagged<User, Int>’)
— 17:36
Alright, so what about our Email type? typealias Email = Tagged<_, String>
— 18:08
What could we possibly tag Email with? There isn’t a clear associated type, as there was with our Subscription.Id and User.Id . Well, we could create a one-off. protocol EmailTag {} typealias Email = Tagged<EmailTag, String> This works just fine, but it’s a little strange. Having empty protocols hanging around feels odd, and why is it a protocol? We could have used an uninhabited enum just as well. enum EmailTag {} typealias Email = Tagged<EmailTag, String>
— 18:50
We could even use a struct, or class, too, which is a bit strange since they can be instantiated. Let’s prefer enums since they can’t. I think what we’re finding is that using Tagged in this way comes with a little bit of baggage in that sometimes we have to create a whole new type to use as a tag.
— 19:04
Other languages have the concept of a “new type”. It’s the ability to create these more type-safe containers in very little code. newtype Email = String
— 19:16
Swift doesn’t have this feature, but it’d be nice, as we wouldn’t have to create brand new types to make things work with Tagged .
— 19:28
We should point out that typealias does not provide this functionality. typealias Email = String The typealias keyword creates a completely interchangeable synonym for a type, so an Email typealias to String would still allow any string to be considered an email and doesn’t provide any additional type-safety.
— 19:48
Now, in lieu of this we’re stuck with RawRepresentable or Tagged . We’ve seen that RawRepresentable is at a bit of a disadvantage, though, without conditional conformance, so we’ll pay the price with the occasional boilerplate creating tags. It’s amazing how we’ve consolidated all of this logic into a single Tagged type. We’re not having to create new RawRepresentable structs everywhere or having to list each Decodable , Equatable protocol we want it to conform to everywhere. Tagged just automatically inherits all the qualities of the type it wraps.
— 20:26
Our Tagged type is working really well! Let’s see what the ergonomics looks like to instantiate a user, which contains a lot of these new types.
— 20:35
What’s nice about our code so far is that our users are instantiated via JSON decoding, we don’t manually call an initializer anywhere. But what if we do want to instantiate a user manually? User.init( id: User.Id.init(rawValue: 1), name: "Blob", email: Email.init(rawValue: " [email protected] "), subscriptionId: Subscription.Id.init(rawValue: 2) )
— 21:15
Yikes, this is kind of a mess. We can clean it up a little bit, though, and use dot-prefix notation. User( id: .init(rawValue: 1), name: "Blob", email: .init(rawValue: " [email protected] "), subscriptionId: .init(rawValue: 2) )
— 21:34
This is still quite a bit of work to manually wrap up all these types.
— 21:42
Swift has a whole collection of ExpressibleBy - Literal protocols that allow us to pass literal values where another type is expected. Maybe we can conditionally conform Tagged to these protocols, as well. extension Tagged: ExpressibleByIntegerLiteral where RawValue: ExpressibleByIntegerLiteral { typealias IntegerLiteralType = RawValue.IntegerLiteralType init(integerLiteral value: IntegerLiteralType) { self.init(rawValue: RawValue(integerLiteral: value)) } } We delegate the IntegerLiteralType type alias to the RawValue ’s IntegerLiteralType , and likewise delegate its initializer that takes integer literals.
— 22:43
That compiles, and allows us to clean up a lot of noise. User( id: 1, name: "Blob", email: .init(rawValue: " [email protected] "), subscriptionId: 2 ) We can even get rid of the noise around email by conforming to ExpressibleByStringLiteral .
— 23:14
We’ve now shown how we can restore the ergonomics of instantiating these type-safe containers without having to manually wrap them everywhere. What’s the point?
— 23:26
We don’t typically do this kind of thing very often! We’re usually content with living in the world of String s and Int s and other basic types in our models. They’re easy to work with and familiar. But now we’re creating a ton of new types everywhere and wrapping all our values in another layer. Is it worth it? It’s totally worth it! The only reason people aren’t doing this is because it wasn’t even really possible till Swift 4.1 became available! Even the RawRepresentable version we started with benefited from a Swift 4.1 feature: automatically deriving Equatable . Swift is giving us a lot of great tools these days that make things possible that would have been otherwise impractical.
— 24:24
Take a look at the code we’ve written. struct Tagged<Tag, RawValue> { let rawValue: RawValue } extension Tagged: Decodable where RawValue: Decodable { init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() self.init(rawValue: try container.decode(RawValue.self)) } } extension Tagged: Equatable where RawValue: Equatable { static func == (lhs: Tagged, rhs: Tagged) -> Bool { return lhs.rawValue == rhs.rawValue } } extension Tagged: ExpressibleByIntegerLiteral where RawValue: ExpressibleByIntegerLiteral { typealias IntegerLiteralType = RawValue.IntegerLiteralType init(integerLiteral value: IntegerLiteralType) { self.init(rawValue: RawValue(integerLiteral: value)) } } This is just twenty lines of code! But imagine if you had to repeat it every time you wanted a new type with these qualities: it wouldn’t be practical. The effort involved means it’s much easier to stick with our raw values everywhere. But now, in Swift 4.1, we can all this functionality, and more with each additional conformance, in a single line! typealias Id = Tagged<User, Int> And while creating new types like this hasn’t been common in Swift, it’s quite common in languages like Haskell and PureScript, where creating a new type is also a single line situation. Compiler-generated code is really cool! We saw it with key paths ( 1 , 2 , 3 ) and we’re seeing it again here.
— 25:15
The bugs this catches are not trivial! We use Tagged in the Point-Free code base and it’s caught bugs that may have otherwise rolled out to our visitors! And we use it everywhere! We use it for ids on our models , ids from third-party services , email addresses , and more ! The compiler keeps us in check and helps us avoid some otherwise nasty bugs.
— 26:02
This is code that folks can use today! You can make your code safer, more expressive and self-documenting with very little code!
— 26:27
One of the things powering Tagged is the notion of “phantom types,” which we only briefly mentioned in passing. We’ll be exploring them more in the future! References Tagged Brandon Williams & Stephen Celis • Apr 16, 2018 Tagged is one of our open source projects for expressing a way to distinguish otherwise indistinguishable types at compile time. https://github.com/pointfreeco/swift-tagged Tagged Seconds and Milliseconds Brandon Williams • Jul 18, 2018 In this blog post we use the Tagged type to provide a type safe way for interacting with seconds and milliseconds values. We are able to prove to ourselves that we do not misuse or mix up these values at compile time by using the tagged wrappers. https://www.pointfree.co/blog/posts/6-tagged-seconds-and-milliseconds Type-Safe File Paths with Phantom Types Brandon Kase, Chris Eidhof, Florian Kugler • Oct 5, 2017 In this Swift Talk episode, Florian and special guest Brandon Kase show how to apply the ideas of phantom types to create a type safe API for dealing with file paths. We’ve used phantom types in our episode on Tagged to provide a compile-time mechanism for distinguishing otherwise indistinguishable types. https://talk.objc.io/episodes/S01E71-type-safe-file-paths-with-phantom-types Downloads Sample code 0012-tagged Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .