Search code examples
swiftgenericscollectionstype-safetytype-alias

How to do type-safe indices in Swift?


I'm trying to do something like this:

typealias HumanId = Int 
typealias RobotId = Int

func getHuman(at index: HumanId) -> Human
func getRobot(at index: RobotId) -> Robot

but as it is now I can call getHuman with RobotId just fine: getHuman(at: RobotId(0)).

How do I make this typesafe?


I understand that I can do something like:

struct HumanId { let id: Int }
struct RobotId { let id: Int }

...and some extra things to make these structs function as indices, but that would lead to some code duplication, and since I'm having more than 2 of these id-types I would like to shorten this somehow, with typealiases and generics perhaps in order to make them unique?


Solution

  • You could leverage Swift generics to achieve your goal. Introduce a generic Index type like this:

    struct Index<T>: RawRepresentable {
        let rawValue: Int
        init(rawValue: Int) { self.rawValue = rawValue }
        init(_ rawValue: Int) { self.rawValue = rawValue }
    }
    

    and then use it like this:

    func getHuman(at index: Index<Human>) -> Human { ... }
    func getRobot(at index: Index<Robot>) -> Robot { ... }
    
    getHuman(at: Index(1))
    getRobot(at: Index(2))
    

    Literal Indices

    You could even use the ExpressibleByIntegerLiteral protocol to provide some syntax sugar when using literal indices:

    extension Index: ExpressibleByIntegerLiteral {
        public init(integerLiteral value: Int) { self.rawValue = value }
    }
    

    For instance:

    getHuman(at: 1)
    getRobot(at: 2)
    

    But the following code will not build, so the solution is still typesafe-ish:

    let someIndex = 123
    getHuman(at: someIndex)
    

    error: cannot convert value of type 'Int' to expected argument type 'Index<Human>'

    Comparable Indices

    As suggested in the comments, we could also add Comparable conformance as well (e.g., so you can use the Index struct as the index in a type conforming to the standard Collection protocol):

    extension Index: Comparable {
        static func < (lhs: Index, rhs: Index) -> Bool {
            lhs.rawValue < rhs.rawValue
        }
    }
    

    Example:

    Index<Human>(1) < Index<Human>(2) // true