swiftgeneric-type-parameters

Generic type method return


In my custom APIClient Swift class, I defined an helper method that extract the value of a given key from a response dictionary, check its type, and throw an error when the value does not correspond to the type I want.

If I allow nil values, I just type String?.self instead of String.self in the method call

static func value<T>(forKey key: String, in dict: [String:Any], type: T.Type) throws -> T? {
    
    let required = !(T.self is ExpressibleByNilLiteral.Type)
    let val = dict[key]
    if val == nil {
        if required {
            throw APIClientResponseError.missing(key: key, in: dict)
        }
        return nil
    }
    if !(val is T) {
        throw APIClientResponseError.invalid(key: key, in: dict)
    }
    return val as? T
}

But I need to keep the T? return type otherwise I'm not able to return nil values. How can I automate the output type based on type parameter? So it can output nil values when giving an optional type, and non-nil values when giving a required type.

Thanks!


Solution

  • I would just write this as two functions. It is much simpler. The caller still can choose whether the value is "required" not by passing in an optional type, but by calling the appropriate function.

    func tryGetValue<T>(forKey key: String, in dict: [String:Any], type: T.Type) -> T? {
        guard let val = dict[key] else {
            return nil
        }
        return val as? T
    }
    
    func value<T>(forKey key: String, in dict: [String:Any], type: T.Type) throws -> T {
        guard let val = dict[key] else {
            throw APIClientResponseError.missing(key: key, in: dict)
        }
        guard let t = val as? T else {
            throw APIClientRespondError.invalid(key: key, in: dict)
        }
        return t
    }
    

    If you really want one function, you can abstract the "return nil or throw error" logic into a protocol. Though to be honest, this is more like a hack than anything I would use in production.

    // Can't really think of a good name for this protocol and its methods...
    protocol APIValue {
        static func missing(in dict: [String: Any], key: String) throws -> Self
        static func invalid(in dict: [String: Any], key: String) throws -> Self
    }
    
    extension Optional: APIValue {
        static func missing(in dict: [String: Any], key: String) throws -> Optional<Wrapped> {
            nil
        }
    
        static func invalid(in dict: [String: Any], key: String) throws -> Optional<Wrapped> {
            nil
        }
    }
    
    extension APIValue {
        static func missing(in dict: [String: Any], key: String) throws -> Self {
            throw APIClientResponseError.missing(key: key, in: dict)
        }
    
        static func invalid(in dict: [String: Any], key: String) throws -> Self {
            throw APIClientResponseError.invalid(key: key, in: dict)
        }
    }
    
    // add extensions for all the other types that can go in the method...
    extension String: APIValue {}
    extension Int: APIValue {}
    extension Double: APIValue {}
    // ...
    
    // now the value method can be:
    func value<T: APIValue>(forKey key: String, in dict: [String:Any], type: T.Type) throws -> T {
        guard let val = dict[key] else {
            return try type.missing(dict: dict, key: key)
        }
        guard let t = val as? T else {
            return try type.invalid(dict: dict, key: key)
        }
        return t
    }