Rather than coercing to [String: Any]
and then trying to work out which values are optional, I would just filter out the nil
values from the [String: Any?]
right off the bat.
For example:
extension Dictionary {
func compactMapValues<U>(
_ transform: (Value) throws -> U?
) rethrows -> [Key: U] {
var result = [Key: U]()
for (key, value) in self {
result[key] = try transform(value)
}
return result
}
}
let d1: [String: Any?] = ["1": Optional(1), "2": nil]
let d2: [String: Any] = d1.compactMapValues { $0 }
print(d2) // ["1": 1]
Presumably the framework you get the [String: Any?]
from is a JSON parsing framework and a nil
value represents a "key": null
association in a JSON object. If you need this information, then filter out the nil
values just before passing the dictionary off to the mapping framework; otherwise I would remove them as soon as you get the dictionary in order to avoid confusion.
But to answer the actual question of:
Any
and Optional
are inherently tricky to use together, as the compiler can both promote an Any
to an Any?
, and erase an Any?
into an Any
.
There’s actually currently an inconsistency in how the compiler and runtime treat casts to optional types. If you try to directly cast an Any
to an Any?
, the compiler will just do optional promotion without checking with the runtime that the operand isn’t already an optional:
let v1: Any? = "123"
let v2: Any = v1 as Any
// Forced cast from 'Any' to 'Any?' always succeeds; did you mean to use 'as'?
let v3 = v2 as! Any?
dump(v3) // Optional(Optional("123")) → some: Optional("123") → some: "123"
However if you use generic functions to hide the fact that the destination type is an optional, then the runtime will check to see if the operand is actually optional and extract it (if not, only then will it perform optional promotion):
func forceCast<T, U>(_ x: T, to _: U.Type) -> U {
return x as! U
}
func optionalPromotingCast<T>(_ x: T) -> T? {
return forceCast(x, to: T?.self)
}
let v1: Any? = "123"
let v2: Any = v1 as Any
let v3: Any? = optionalPromotingCast(v2)
dump(v3) // Optional("123") → some: "123"
let nonOptional: Any = "123"
let v4: Any? = optionalPromotingCast(nonOptional)
dump(v4) // Optional("123") → some: "123"
IMO the runtime has the correct behaviour, and v2 as! Any?
should do the same thing as the generic workaround; it should check the dynamic type of v2
first and perform optional promotion last.
You could easily argue against this though, as this is a cast that always succeeds, and casts that always succeed are done using the as
operator in Swift. If as
checked if the operand was optional before optional promotion, then it would be inconsistent with the implicit optional promotion behaviour when assigning an Any
to an Any?
. Like I said earlier, this is an inherently tricky problem.
For now though you should be able to use the generic workaround to solve your problem; though I would still advise avoiding the problem altogether by filtering out the nil
values when you have an easy chance to do so.