if object is TargetType. To verify the type and safely access its members at the same time, use conditional downcasting with if let instance = object as? TargetType.Why this happens
Swift is a statically typed language designed around strong compile-time guarantees. In standard application code, the compiler tracks the concrete type of every variable, preventing mismatched operations before your code ever runs.
However, working with heterogeneous collections, legacy Objective-C APIs, parsed JSON trees, or system frameworks often forces you to work with existential types like Any or AnyObject. When an item’s type is erased to Any, the compiler limits you to operations available on all types, preventing direct access to properties or methods specific to the underlying concrete class or struct.
To safely inspect or interact with the wrapped value, you must query its type at runtime. Swift provides dedicated runtime operators—namely is, as?, and as!—as well as pattern matching features to determine whether an instance conforms to or inherits from a specific type.
How to fix it
1. Test for a type using the is operator
When you only need a Boolean check to confirm whether an instance belongs to a class, struct, enum, or protocol—and you do not need to call its methods immediately—use the is operator.
let items: [Any] = ["Apples", 42, ["iOS", "macOS"], 3.14]
for item in items {
if item is String {
print("Found a string")
} else if item is [String] {
print("Found an array of strings")
}
}
You can also use is inside closure predicates to filter collections:
let mixedList: [Any] = [10, "Title", true, [1, 2, 3], "Subtitle"]
// Keep only elements that are arrays
let arraysOnly = mixedList.filter { $0 is [Int] }
print(arraysOnly.count) // Outputs: 1
Note that is also returns true if the tested instance is a subclass of the target class or conforms to the target protocol.
2. Conditionally cast and unwrap with as?
Most of the time, checking a type is only the first step toward using its properties. If you use is, you would still have to cast the object afterward to call its methods. The idiomatic Swift approach combines the check and the cast into a single operation using optional downcasting (as?).
let payload: [Any] = ["Welcome", [100, 200], 404]
for item in payload {
if let text = item as? String {
// 'text' is now strongly typed as String
print("Text length: \(text.count)")
} else if let numberArray = item as? [Int] {
// 'numberArray' is strongly typed as [Int]
print("Sum: \(numberArray.reduce(0, +))")
}
}
If the downcast fails, as? evaluates to nil, cleanly bypassing the conditional block without runtime crashes.
3. Filter and cast collections using compactMap
If you have an array containing mixed types and want a new strongly typed array containing only instances of one specific type, combining compactMap with as? is faster and cleaner than filtering and re-casting.
let rawData: [Any] = ["First", 12, "Second", false, "Third"]
// Extracts only String elements and types the result as [String]
let stringsOnly: [String] = rawData.compactMap { $0 as? String }
print(stringsOnly) // ["First", "Second", "Third"]
4. Inspect multiple possible types with switch
When handling an object that could be one of many distinct types, chaining multiple if let blocks can become unreadable. A switch statement with is or as pattern matching provides an exhaustive and clean structure.
func inspectValue(_ value: Any) {
switch value {
case let stringValue as String:
print("String with length: \(stringValue.count)")
case let intValue as Int where intValue > 0:
print("Positive integer: \(intValue)")
case is Double:
print("Floating point number")
case let array as [Any]:
print("Collection with \(array.count) items")
default:
print("Unrecognized type: \(type(of: value))")
}
}
5. Obtain the dynamic metatype with type(of:)
If you need to inspect or print the dynamic type name rather than compare against a fixed type, use the type(of:) function.
let mysteryObject: Any = UIView()
let actualType = type(of: mysteryObject)
print(actualType) // Prints: UIView
// Exact metatype identity comparison
if type(of: mysteryObject) == UIView.self {
print("Exact instance of UIView, not a subclass")
}
Be aware of the difference between is and type(of:) == Target.self: is returns true for subclasses, whereas the metatype identity check requires an exact match.
How to prevent it
- Favor generic constraints over
Any: Design function signatures with generic parameters<T>or protocol constraints rather than acceptingAnyorAnyObject. - Use Swift enums with associated values: When modeling data that can represent one of several known types, define an enum rather than using untyped collections.
- Avoid force-downcasting (
as!): Do not useas!unless you have an invariant guaranteed by the compiler or storyboard architecture; failing casts trigger immediate runtime fatal errors. - Leverage
Codablefor external data: Decode network payloads into structured data models usingJSONDecoderinstead of traversing raw dictionary and array structures manually.
FAQ
What is the difference between is and as? in Swift?
The is operator performs a type check and returns a Boolean indicating whether an instance is of the specified type. The as? operator performs a conditional downcast, returning an optional containing the casted value if the type matches, or nil if it does not.
Does is return true for protocol conformances and subclasses?
Yes. If an instance belongs to a class that inherits from the target class, or if its type conforms to the target protocol, evaluating instance is TargetType returns true.
Can I use is to check generic types at runtime?
You can check against concrete generic types (such as item is [String] or item is [Int]), but Swift’s runtime cannot evaluate unspecialized generic abstractions (like item is Array without an element type specification) unless cast against a non-generic protocol.
Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.
