Tagged

repository·main·Indexed 23 days ago

https://github.com/pointfreeco/swift-tagged

A 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.

Tokens
1.7K
Snippets
5
Records
10
Agent score
32%

What's inside swift-tagged

  1. What is Tagged and why use it?

    main
    Tagged is a wrapper type designed to differentiate between seemingly equivalent values at the type level. It helps prevent runtime bugs and security issues by ensuring that values like 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.
  2. How to handle tag collisions in Tagged

    main

    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:

    1. Uninhabited Enums: Create a small enum for each tag. This is explicit and nestable.
    2. Tuple Labels: Use a tuple with a label as the tag. This is more succinct as tuple labels are encoded in the type system.
    // 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>
    }
  3. Why use Tagged instead of Type Aliases or Protocols?

    main

    Tagged provides type safety that other Swift patterns do not:

    • Type Aliases: These are interchangeable with the original type and offer no additional safety or guarantees.
    • Protocols (e.g., 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.
  4. Features of Tagged: Protocol Conformance

    main

    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:

    • Equatable: Compare tagged values directly if the raw values are equatable.
    • Hashable: Use tagged values in Set or as Dictionary keys.
    • Comparable: Sort tagged values directly.
    • Codable: Encode and decode tagged values seamlessly.
    • ExpressibleByLiteral: Use literals (like 1 or "string") to instantiate tagged types.
    • Numeric: Numeric tagged types support mathematical operations.
  5. How to use Tagged to differentiate types

    main

    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)
  6. Install Tagged via Swift Package Manager

    main

    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")
    ]
  7. Use TaggedMoney for currency unit safety

    main

    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 // ✅ true
  8. Use TaggedTime for unit safety

    main

    The 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