What is Tagged and why use it?
mainUser.Id (an Int) cannot be accidentally used where a Subscription.Id (also an Int) is expected. By wrapping basic types in specific contexts, you move domain logic validation from runtime to compile time.repository·main·Indexed 23 days ago
https://github.com/pointfreeco/swift-taggedA Swift library providing a wrapper type to differentiate between values of the same underlying type at compile time. It enhances type safety and domain modeling by preventing the accidental use of equivalent types, such as different ID types. The library includes conditional protocol conformance for Equatable, Hashable, Comparable, Codable, and Numeric, as well as specialized nanolibraries TaggedTime and TaggedMoney for unit safety.
User.Id (an Int) cannot be accidentally used where a Subscription.Id (also an Int) is expected. By wrapping basic types in specific contexts, you move domain logic validation from runtime to compile time.If you need to tag multiple values of the same underlying type (e.g., two different String fields) within the same struct, you cannot reuse the same tag. You have two primary ways to resolve this:
enum for each tag. This is explicit and nestable.// Option 1: Uninhabited Enums
struct User {
enum EmailTag {}
enum AddressTag {}
typealias Email = Tagged<EmailTag, String>
typealias Address = Tagged<AddressTag, String>
}
// Option 2: Tuple Labels
struct User {
typealias Email = Tagged<(User, email: ()), String>
typealias Address = Tagged<(User, address: ()), String>
}struct User {
let id: Id
let email: Email
let address: Address
let subscriptionId: Subscription.Id?
typealias Id = Tagged<User, Int>
enum EmailTag {}
typealias Email = Tagged<EmailTag, String>
enum AddressTag {}
typealias Address = Tagged<AddressTag, String>
}
// OR using tuple labels
struct User {
let id: Id
let email: Email
let address: Address
let subscriptionId: Subscription.Id?
typealias Id = Tagged<User, Int>
typealias Email = Tagged<(User, email: ()), String>
typealias Address = Tagged<(User, address: ()), String>
}Tagged provides type safety that other Swift patterns do not:
RawRepresentable): While useful, protocols cannot be extended conditionally. Using a protocol requires manual implementation of Equatable, Hashable, Decodable, and Encodable. Tagged provides these (and more, like Comparable and ExpressibleByLiteral) automatically without the boilerplate.Tagged uses conditional conformance to inherit protocols from its underlying raw value. If the raw value conforms to a protocol, the tagged type will as well:
Set or as Dictionary keys.1 or "string") to instantiate tagged types.To differentiate types, use Tagged<Tag, RawValue>. A common pattern is to use the container type itself as the Tag to make each ID unique to its parent struct.
import Tagged
struct User {
let id: Id
typealias Id = Tagged<User, Int>
}
struct Subscription {
let id: Id
typealias Id = Tagged<Subscription, Int>
}Now, a function expecting Subscription.Id will reject a User.Id at compile time.
import Tagged
struct User {
let id: Id
let email: String
let address: String
let subscriptionId: Subscription.Id?
typealias Id = Tagged<User, Int>
typealias Email = Tagged<User, String>
}
struct Subscription {
let id: Id
typealias Id = Tagged<Subscription, Int>
}
func fetchSubscription(byId id: Subscription.Id) -> Subscription? {
return subscriptions.first(where: { $0.id == id })
}
// This will now fail to compile if you pass user.id:
// let subscription = fetchSubscription(byId: user.id)You can add Tagged to an Xcode project by adding it as a package dependency using the following URL:
https://github.com/pointfreeco/swift-tagged
To use Tagged in a SwiftPM project, add it to your Package.swift file within the dependencies array. Use the following URL and version constraint:
dependencies: [
.package(url: "https://github.com/pointfreeco/swift-tagged", from: "0.6.0")
]RawRepresentable interface. You can access the underlying value using the .rawValue property or instantiate a new tagged value using init(rawValue:).The TaggedMoney nanolibrary provides Dollars<A> and Cents<A> types to distinguish between whole dollar amounts and fractional cents. Note that these types track units, not specific currencies (like USD or EUR).
import TaggedMoney
struct Prize {
let amount: Dollars<Int>
let name: String
}
let moneyRaised: Cents<Int> = 50_000
let isLess = theBigPrize.amount.cents < moneyRaised // ✅ trueThe TaggedTime nanolibrary provides Milliseconds<A> and Seconds<A> types to prevent mixing up time units. It allows you to convert between units safely using properties like .milliseconds or .seconds.
import TaggedTime
struct BlogPost: Decodable {
typealias Id = Tagged<BlogPost, Int>
let id: Id
let publishedAt: Seconds<Int>
let title: String
}
let futureTime: Milliseconds<Int> = 1528378451000
let isBefore = breakingBlogPost.publishedAt.milliseconds < futureTime // ✅ true