Type confusion with Codable

Hi,

I am using a property wrapper that simplifies the access to UserDefaults, it is based off this tutorial.
I wanted to alter the wrappedValue property's getter and setter so that they use different methods to store/retrieve values depending on whether the underlying type is codable or not.

var wrappedValue: Value {
        get {
            if Value.self is Codable {
                let value = storage.object(forKey: key) as? Value
                return value ?? defaultValue
            } else {
                let value = storage.value(forKey: key) as? Value
                return value ?? defaultValue
            }
        }
        
        set {
            if let optional = newValue as? AnyOptional, optional.isNil {
                storage.removeObject(forKey: key)
            } else {
                if Value.self is Codable {
                    storage.set(object: newValue, forKey: key) // <-- Complains here
                } else {
                    storage.set(newValue, forKey: key)
                }
            }
        }
    }

In the getter part I check if the type conforms to Codable and then use a different method. That works perfectly.
But in the setter part I wanted to do the same and used the same logic, but there the compiler keeps complaining that the Value type needs to conform to Encodable. My set method does indeed require the value to be Encodable:

func set<Object>(object: Object, forKey key: String) throws where Object: Encodable

But isn't that guaranteed by the Value.self is Codable line in the if statement already?

What am I missing here?

Not statically, as the compiler expects. The "knowledge" of the conformance isn't carried forward (at compile time) from the if test to the next statement.

You could do something like this instead:

                if let codableNewValue = newValue as? Codable {
                    storage.set(object: codableNewValue, forKey: key)
                }

Thank you @QuinceyMorris for your answer!
Unfortunately I already tried that approach, but it was unsuccessful.

I then get the error:

Protocol 'Codable' as a type cannot conform to 'Encodable'

Its quite the same if I try casting to Encodable instead.

Oh, yes, that's the "protocol type doesn't conform to the protocol" problem. It's kinda annoying with Codable, and I don't know there's an easy solution. Maybe if you can post a more complete example, we can fiddle with code a bit? :slight_smile:

Ok,
my setup consists of a property wrapper that stores values in UserDefaults:

private protocol AnyOptional {
    var isNil: Bool { get }
}

extension Optional: AnyOptional {
    var isNil: Bool { self == nil }
}

@propertyWrapper
struct UserDefaultsBacked<Value> {
    let key: String
    let defaultValue: Value
    var storage: UserDefaults = .standard
    
    var wrappedValue: Value {
        get {
            if Value.self is Codable {
                let value = storage.object(forKey: key) as? Value
                return value ?? defaultValue
            } else {
                let value = storage.value(forKey: key) as? Value
                return value ?? defaultValue
            }
        }
        
        set {
            if let optional = newValue as? AnyOptional, optional.isNil {
                storage.removeObject(forKey: key)
            } else {
                if let newValue = newValue as? Codable {
                    storage.set(object: newValue, forKey: key)
                } else {
                    storage.set(newValue, forKey: key)
                }
            }
        }
    }
}

I extended UserDefaults to be able to store Codable values:

extension UserDefaults: CodableSavable {
    func set<Object>(object: Object, forKey key: String) throws where Object: Encodable {
        do {
            let data = try RC.encoder.encode(object)
            set(data, forKey: key)
        } catch {
            throw CodableSavableError.unableToEncode(reason: error)
        }
    }
    
    func get<Object>(objectOf type: Object.Type, forKey key: String) throws -> Object where Object: Decodable {
        guard let data = data(forKey: key) else {
            throw CodableSavableError.noValue
        }
        
        do {
            let object = try RC.decoder.decode(type, from: data)
            return object
        } catch {
            throw CodableSavableError.unableToDecode(reason: error)
        }
    }
}

I then would like to store a dict with a codable key and a String at some point:

enum SomeKey: String, Codable {
    case keyA, keyB
}
...
@UserDefaultsWrapped(key: "test", defaultValue: [:])
var dict: [SomeKey: String]
...
dict[.keyA] = "newValue"

The problem you're running into here is a combination of two core issues:

  1. Generic methods in Swift need static (compile-time) knowledge of their generic arguments in order to be able to dispatch
  2. The Swift type checker does not "refine" types which pass type checks:
    • For values:

      let x: Optional<T> = T(...)
      if x != nil {
          // x here is still `Optional<T>`, not `T`
      }
      
    • For types:

      func foo<T>(_ value: T) {
          if T.self is SomeProtocol.Type {
              // `T` is _dynamically_ known to be `SomeProtocol`, but not statically.
              // `value` is still not considered to conform to `SomeProtocol`
          }
      }
      

In order to call your

you would need the compiler to know statically that your Object is Encodable. A check like

if Value.self is Codable {
    storage.set(object: newValue, forKey: key) // <-- Complains here
}

is insufficient, because (1) it's done at runtime, not compile time, and (2) the type of newValue has not effectively changed. This is why @QuinceyMorris's suggestion doesn't work either: the cast is dynamic, and the compiler still needs compile-time knowledge of both Object and Encodable conformance.

Ideally, you'd spell this out as:

if let encodableValue = value as? Value & Encodable {
    // Ideally, `encodableValue` would have both constraints applied to
    // it statically.
}

but Swift doesn't support this type of composition and check right now.


There are a few ways you could work around this, but it depends on your use-case. Do you have types which are not Codable which you expect to write to user defaults in this way? If not, then the easy solution would be to simply constrain Value to be Codable at the type level and call it a day:

@propertyWrapper
struct UserDefaultsBacked<Value: Codable> {
    // No `Codable` checks necessary: you'll be able to call
    // `storage.object(forKey:)` and `storage.setObject(_:forKey:)`
    // directly.
}

Since you took the time to write a fallback for handling non-Codable values, I suspect that it isn't that easy, but knowing about the types you intend to decorate here would help inform a solution. (One other simple, yet somewhat annoying option: write one version which constrains to Codable, and one version which does not — use as appropriate.)

1 Like

Thank you @itaiferber for your explanation!
Your post made me think about my setup and after reviewing my current usage of UserDefaultsBacked I came to the conclusion that your last proposed solution is probably the easiest and best approach for me right now. It doesn't really make a difference for me right now to constraint the Value type to Codable and it is probably the safest solution for me as well.

1 Like