swiftgenericsoption-typeswift-keypath

Unwrapping Optional types in a Generic method in Swift 5


I'm trying to create an instance of type B from an instance of type A, however some of the properties of the original type A are optional and the creation of B should throw an error if that occurs.

My issue is I can't tell if type T is optional, and if it is, how to unwrap it. I'm trying the following, but swift can't tell what the types should be...

The example below is contrite, the real sample has almost a hundred values.

struct A {
  let name: String?
  let price: Price?
  
  struct Price {
    let value: Double?
  }
}

struct B {
  let name: String
  let priceValue: Double
}

extension A {
  func convert() throws -> B {
    do {
      let name: String = try unwrap(\.name) // error: Type of expression is ambiguous without more context
      let priceValue: Double = try unwrap(\.price.value) // error: Type of expression is ambiguous without more context

      return B(name: name, priceValue: priceValue)
    }
  }

  func unwrap<U, T>(_ path: KeyPath<A, T>) throws -> U {

    let value = self[keyPath: path] // value is of type T

    if let value = value as? U {
      return value
    } else {
      throw Error.missing("KeyPath '\(path)' is 'nil'")
    }
  }

  enum Error: Swift.Error {
    case missing(String?)
  }
}

The following I know will work, but I'd rather not repeat this 100s of times in the code?


extension A {
  func convertWithConditionals() throws -> B {
    do {
      guard let name = self.name else {
        throw Error.missing("KeyPath 'name' is 'nil'")
      }

      guard let priceValue = self.price?.value else {
        throw Error.missing("KeyPath 'price.value' is 'nil'")
      }
     
      return B(name: name, priceValue: priceValue)
    }
  }
}

There must be some... swift-y way of doing this that I'm not thinking of.


Solution

  • If the intention is to call unwrap() only with key paths leading to an optional property then you can declare the argument type as KeyPath<A, T?>, and the second placeholder type U is not needed:

    func unwrap<T>(_ path: KeyPath<A, T?>) throws -> T {
        if let value = self[keyPath: path] {
            return value
        } else {
            throw Error.missing("KeyPath '\(path)' is 'nil'")
        }
    }
    

    The usage can be simplified to

    func convert() throws -> B {
        let name = try unwrap(\.name)
        return B(name: name)
    }
    

    or just

    func convert() throws -> B {
        return try B(name: unwrap(\.name))
    }