Casting from Any to Optional

Currently (in Swift 4.2.1), it is an error to cast a value from Any to Optional, even when that value was created as the exact same Optional type and previously cast to Any. Here is an example:

let x: Int? = 1
let y: Any = x as Any
print(type(of: y))          // "Optional<Int>\n"

let z: Int? = y as! Int?    // error: cannot downcast from 'Any' to a more optional type 'Int?'

Is this a bug, or an oversight, or an intentional design decision? In particular, would it require an evolution proposal to make it possible for this to work? My expectation was that Any could wrap any type, and be unwrapped back to that type.

So far the only way I have found to extract the original Optional value, is via a generic helper function such as:

func unwrap<T>(_ x: Any) -> T {
  return x as! T
}

let z: Int? = unwrap(y)
print(type(of: z))          // "Optional<Int>\n"

It seems rather peculiar that we can attempt to unwrap an Any into an unspecified generic type, but we are prohibited from unwrapping an Any into the exact specific type we know it contains, if that type is Optional.

3 Likes

I agree that this feels like an arbitrary limitation, especially given that this works exactly as expected:

let maybeInt = 1 as Int?
let anything = maybeInt as Any

let opened = anything as? Int?
// error: cannot downcast from 'Any' to a more optional type 'Int?'

func dynamicCast<T>(_ value: Any, to _: T.Type) -> T? {
  if let value = value as? T {
    return value
  } else {
    return nil
  }
}

if let opened = dynamicCast(anything, to: Int?.self) {
  print("got an optional Int: \(opened)")
} else {
  print("it's something else entirely")
}
2 Likes

I think we added this to stop people from expecting x as! Int? to act like x as? Int. It's not really a correct error when casting from any (non-class) existential / protocol value (Any is "no protocols").

Could you elaborate? I run into the error described here multiple times, but I've never tried to do x as! Int? instead of x as? Int. Why would someone think that they're the same?

:-/ I don't have much here, but I remember it coming up in the first few years of Swift. It's also logic shared with x as? Int?, which is a more reasonable typo to make (because you want the result type to be optional). The compiler could certainly make a different decision about diagnosis based on as? vs. as!.

I needed some code to test if an Any is nil, which I thanks to this thread managed:

func isNil(_ x : Any) -> Bool {
    func unwrap<T>() -> T {
        return x as! T
    }
    let y : Any? = unwrap()
    return y == nil
}

Is there a less convoluted way of achieving this?

There is no light-weight built-in way of doing this, but a common approach that just might meet your criteria for convolution is to use a custom protocol (often scoped internal or even fileprivate).

protocol _Optional {
    var isNil: Bool { get }
}

extension Optional: _Optional {
    var isNil: Bool { return self == nil }
}

func isNil (_ input: Any) -> Bool {
    return (input as? _Optional)?.isNil ?? false
}

isNil(nil as String?) // true
isNil("") // false

I too just ran into this error trying to cast Any as an optional and would have liked for it to work, so I just wanted to add my vote for allowing casting from Any to Optional<SomeType> if there's no harm in it.

To be more specific, what I want is that when I have a value of type Any I can distinguish between nil of different types:

let nilInt: Int? = nil
let nilString: String? = nil

func isNilString (_ value: Any) -> Bool {
    guard let optionalString = value as? String? else { return false }
    return optionalString == nil
}

isNilString(nilInt) // Should return false
isNilString(nilString) // Should return true

Any updates here? Who can we talk to to get this going?

maybe we could allow as Optional<T> but not as T? or to allow as (T?). just to make sure the user knows what they are doing.

You can currently do a double cast if you want:

let x = Int?.some(3) as Any

if let xx = x as Any? as? Int? {
  // here 'xx' is 'Int?'
}
2 Likes

Works in Xcode Version 12.5 beta 3 (12E5244e)

z is back to Optional(1)

3 Likes

You can always add a failable initializer to Optional that does the conversion:

let x: Int? = 1
let y: Any = x as Any
print(type(of: y))          // "Optional<Int>\n"

extension Optional {
    init?(from any: Any) {
        guard let opt = any as Any? as? Self else {
            return nil
        }
        self = opt
    }
}

let z: Int? = Optional(from: y)!
1 Like

Nice find! Yes, with Swift 5.4 we can now cast from Any to T? without intermediate steps.

2 Likes

That's good. Does anyone know why it wasn't working before?

Terms of Service

Privacy Policy

Cookie Policy