Using typed string constants as keys in a Dictionary

I've defined a string type like this:

	public
	struct
	Key : RawRepresentable, Equatable, Hashable
	{
		public private(set) var rawValue: String
		public init(rawValue: String) { self.rawValue = rawValue }
		public init(_ rawValue: String) { self.rawValue = rawValue }
	}

	static let code    =   Key("code")

And I'd like to use these as keys in a dictionary. But the dictionary comes from Objective-C, and I'm not sure how to handle the type coercion. E.g., I'd like to do something like this:

	@objc
	foo(dictionary inDict: [String:AnyObject])
	{
		if let errorCode = inDict[.code]
		{
		}
	}

I want the dictionary key to be of type Key, so that the compiler enforces the use of the symbolic constants. Is there a way to cast [String:AnyObjet] to [Key:AnyObjet]? It can't be the parameter type, because Objective-C can't call it that way (according to the compiler).

Is this what you are looking for?:

extension Dictionary where Key == String {
    public subscript(_ key: MyModule.Key) -> Value {
        // “MyModule.Key” because “Key” alone clashes with Dictionary’s generic “Key”.
        get {
            return self[key.rawValue]
        }
        set {
            self[key.rawValue] = newValue
        }
    }
}

It would allow you to do this:

let dictionary: [String: Any] = [:]
dictionary[.code] = true
if dictionary[.code] == true {
    print("Stored and retrieved with a strongly typed “Key”.")
}

If you also want to make it impossible to use the raw strings, you would instead have to map the keys and vend it as [Key: AnyObject]:

public func getDictionary() -> [Key: AnyObject] {
    let underlying = getFromObjectiveC()
    let pairs = underlying.lazy.map({ (Key($0), $1) })
    return Dictionary(pairs, uniquingKeysWith: { first, _ in first })
}

Note that this will be inefficient, especially if you need to repeatedly go back and forth. And It is still impossible to stop indirect string usage like this:

let dictionary = getDictionary()
dictionary[Key(rawValue: "code")] = true

P.S. Unless there are contextual constraints you have not mentioned, this would be a much more concise way to define your Key type:

public enum Key : String {

    case code
    // raw value is also “code”

    case modified = "overridden"
    // raw value is “overridden” instead of “modified”.
}

I was hoping to avoid extending Dictionary; I was hoping I could just cast to the desired type. It's okay, I think I'm going to go a different way altogether. The enum might be fine. It just would've been cool to be able to type a period and use code completion to get the available key names.

Maybe this?

Objective-C Key.h
typedef NSString * CustomKey NS_TYPED_ENUM;

NSDictionary<CustomKey, id> *dict = @{ @"code": @"test" };
extension CustomKey {
  static let code = CustomKey(rawValue: "code")
}

let dict: [CustomKey: Any] = [.code: "test"]

In Objective-C, CustomKey is just NSString. In Swift CustomKey is a RawRepresentable struct.

The NS_TYPED_ENUM approach is the best path assuming you can control the Objective-C side. If not, I think the best approach is to wrap the dictionary in a struct which you can index with a strongly-typed key as a client; the implementation of the struct just calls through to the dictionary with the corresponding string value. This does not incur 'bridging' costs like the map approach as you aren't manipulating the Dictionary storage.

struct CustomStruct {
    private let dict: [String : AnyObject]

    init(from dict: [String : AnyObject]) {
        self.dict = dict
    }

    subscript (_ key: Key) -> AnyObject? {
        return self.dict[key.stringValue]
    }

    enum Key : String { // this could be a `RawRepresentable` struct if you want it to be
        case code
    }
}

You could also do fancier stuff in the struct like make generic subscripts or validating the dictionary with a failable initializer.

1 Like

I do control the Objective-C code, as well, but the initial dictionary comes from NSJSONSerialization.

Test.h
+ (NSDictionary<CustomKey, id> *)dictFrom:(NSData *)data;
Test.m
+ (NSDictionary<CustomKey, id> *)dictFrom:(NSData *)data {
  return [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil];
}
let dict = Test.dict(from: data) // [CustomKey: Any]
1 Like