How to Print the Type of Any Variable When Debugging Swift Code

How to Print the Type of Any Variable When Debugging Swift Code

Written by

in

Quick answer: To print the runtime type of any variable in modern Swift, pass the instance to type(of:) inside a print statement: print(type(of: myVariable)). For fully qualified module-prefixed names or debugging complex generic structures, wrap the metatype in String(reflecting: type(of: myVariable)).

Why this happens

Developers frequently need to inspect the concrete type of an instance at runtime. This need commonly arises when working with polymorphic class hierarchies, protocol types (any Protocol), ambiguous Any or AnyObject values, and complex generic wrappers such as Optional. During code inspection and debugging, knowing the runtime type helps identify unexpected nil bridging, subclass substitutions, or incorrect deserialization.

In early iterations of the Swift language (Swift 1.x and 2.x), developers accessed an instance’s runtime type using property accessors like .dynamicType or private standard library routines like _stdlib_getDemangledTypeName(). These legacy APIs caused confusion because .dynamicType behaved like a property access on an instance, yet returned a metatype object whose .description selector was only available on Objective-C bridged classes.

Starting in Swift 3, Apple removed the .dynamicType keyword completely. Swift replaced it with the global type(of:) function. Because Swift metatypes conform to custom string convertible protocols within the standard library, printing a metatype directly via print() or string interpolation produces a clean, human-readable demangled type name without requiring external helper functions.

How to fix it

1. Use the type(of:) function (Recommended)

The standard, idiomatic way to determine and print the runtime type of any instance in modern Swift is the global type(of:) function. You pass your variable directly into type(of:) and output it with print() or include it in string interpolation.

import Foundation

class Vehicle {}
class ElectricCar: Vehicle {}

let count: Int = 42
let greeting: String = "Hello"
let activeCar: Vehicle = ElectricCar()
let tags: [String] = ["swift", "types"]

print(type(of: count))      // Prints: Int
print(type(of: greeting))   // Prints: String
print(type(of: activeCar))  // Prints: ElectricCar
print(type(of: tags))       // Prints: Array<String>

Notice that even though activeCar is statically typed as the superclass Vehicle, type(of: activeCar) correctly inspects the instance at runtime and prints its dynamic subtype ElectricCar.

2. Print statically known types using .self

If you want to print the type name without holding an active instance of that type, access the metatype directly using .self on the type name:

print(Double.self)              // Prints: Double
print((String?).self)           // Prints: Optional<String>
print([Int: String].self)       // Prints: Dictionary<Int, String>
print(Array<Float>.self)        // Prints: Array<Float>

This is particularly helpful when checking the concrete signature of generic constraints or mapping schema types in persistence libraries.

3. Use String(reflecting:) for fully qualified names

Standard printing formats types using short names without module prefixes. In larger applications with multiple internal frameworks or overlapping class names (for example, a custom User model versus an external SDK’s User model), short names can mask subtle namespace conflicts.

To output the fully qualified namespace alongside the demangled type, wrap type(of:) inside String(reflecting:):

import Foundation

struct ProfileRecord {}

let record = ProfileRecord()
let timestamp = Date()

// Standard output
print(String(describing: type(of: record))) 
// Prints: ProfileRecord

// Fully qualified output
print(String(reflecting: type(of: record))) 
// Prints: YourAppModuleName.ProfileRecord

print(String(reflecting: type(of: timestamp))) 
// Prints: Foundation.Date

String(reflecting:) accesses the CustomDebugStringConvertible representation of the metatype, revealing the parent module or framework where the type is declared.

4. Inspect types hidden behind Any and protocol existentials

When consuming data from JSON parsers, property lists, or legacy Objective-C APIs, values often arrive boxed as Any or AnyObject. Using type(of:) reveals the concrete underlying storage rather than the container interface:

import Foundation

let payload: [String: Any] = [
    "id": 101,
    "threshold": 98.6,
    "isActive": true,
    "data": Data()
]

for (key, value) in payload {
    let actualType = type(of: value)
    print("Key: \(key) -> Concrete Type: \(actualType)")
}
// Output:
// Key: id -> Concrete Type: Int
// Key: threshold -> Concrete Type: Double
// Key: isActive -> Concrete Type: Bool
// Key: data -> Concrete Type: Data

5. Inspect dynamic types inside the Xcode LLDB debugger

If you are paused at a breakpoint in Xcode and want to print the type without modifying or recompiling code, run one of the following commands in the LLDB console:

  1. Use the expression command with type(of:):
    text
    (lldb) po type(of: myVariable)
  2. Inspect the memory and object structure with frame variable:
    text
    (lldb) frame variable -T myVariable

    The -T flag forces LLDB to display the explicit type information for the variable alongside its value.

How to prevent it

  • Do not use old syntax like .dynamicType or mangled symbols like _stdlib_getDemangledTypeName(); both are obsolete and removed from modern Swift compilers.
  • Use String(reflecting:) when logging types in multi-target modular architectures to prevent confusion between identical class names across targets.
  • Avoid using runtime string comparisons of type names (e.g., if "\(type(of: x))" == "MyClass") to perform program logic. Use the type checking operators is or as? instead.
  • Remember that optionals wrap their payload. Printing type(of: optionalValue) on a variable containing an integer yields Optional<Int>, not Int.

FAQ

Why does type(of: value) show Optional<SomeType> instead of the wrapped type?

In Swift, an optional variable is an instance of the Optional enumeration, even when it contains a non-nil value. Because type(of:) evaluates the dynamic metatype of the variable passed to it, it returns the enum container Optional<Wrapped> unless you explicitly force-unwrap or bind the variable first.

How do I check if a variable matches a specific type instead of printing it?

To verify whether an object conforms to or matches a specific type programmatically, use the is operator (if object is MyClass) or safe conditional downcasting with as? (if let specific = object as? MyClass). String comparisons of printed type names are fragile and easily broken by code refactoring or module renames.

Does type(of:) incur a performance penalty in production builds?

Calling type(of:) simply fetches the metadata pointer associated with the instance, which is a lightweight operation. However, formatting that metatype into a demangled string using String(describing:) or print() requires string allocation and metadata lookup, so avoid running metatype string conversions inside tight loops in release code.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 3.0; this article is shared under the same license.