Swift Type Casting

Type casting checks or converts the type of an instance at runtime. Swift gives you three tools: is to check a type, as? to safely attempt a conversion, and as! to force one. Think of it like checking what is inside a sealed box — you first verify the type, then decide how to handle its contents.

Why Type Casting Is Needed

When different types share a common parent class or protocol, you sometimes store them together in one collection. Type casting lets you discover and use the specific type of each item at runtime.

class Animal {
    var name: String
    init(name: String) { self.name = name }
}

class Dog: Animal {
    func fetch() { print("\(name) fetches!") }
}

class Cat: Animal {
    func purr() { print("\(name) purrs...") }
}

let animals: [Animal] = [Dog(name: "Rex"), Cat(name: "Whiskers"), Dog(name: "Buddy")]

Checking Type With is

The is operator returns true if an instance belongs to a given type or any of its subclasses.

for animal in animals {
    if animal is Dog {
        print("\(animal.name) is a dog.")
    } else if animal is Cat {
        print("\(animal.name) is a cat.")
    }
}
// Output:
// Rex is a dog.
// Whiskers is a cat.
// Buddy is a dog.

Diagram: Type Hierarchy and Casting

        Animal (superclass)
        /         \
      Dog         Cat
    fetch()     purr()

animals array: [Dog, Cat, Dog]  — stored as [Animal]

animal is Dog   → true/false
animal as? Dog  → Dog? (optional)
animal as! Dog  → Dog  (crash if wrong)

Downcasting With as?

A downcast converts a parent type to a more specific child type. Use as? for a safe optional result — it returns nil if the cast fails instead of crashing.

for animal in animals {
    if let dog = animal as? Dog {
        dog.fetch()
    } else if let cat = animal as? Cat {
        cat.purr()
    }
}
// Output:
// Rex fetches!
// Whiskers purrs...
// Buddy fetches!

Force Downcasting With as!

Use as! only when you are 100% certain the cast will succeed. A failed force cast crashes your app immediately.

let first = animals[0] as! Dog   // Safe — we know index 0 is a Dog
first.fetch()   // Output: Rex fetches!

Upcasting With as

Upcasting converts a child type to its parent. This always succeeds, so no ? or ! is needed.

let myDog = Dog(name: "Max")
let asAnimal = myDog as Animal   // Always safe

print(asAnimal.name)   // Output: Max
// asAnimal.fetch()    // ← Error: Animal has no fetch()

Type Casting With Any and AnyObject

Any — Holds Any Type

Any can hold a value of absolutely any type, including structs, enums, and functions.

var mixed: [Any] = [42, "hello", true, 3.14, Dog(name: "Spot")]

for item in mixed {
    switch item {
    case let number as Int:
        print("Int: \(number)")
    case let text as String:
        print("String: \(text)")
    case let flag as Bool:
        print("Bool: \(flag)")
    case let decimal as Double:
        print("Double: \(decimal)")
    case let dog as Dog:
        print("Dog: \(dog.name)")
    default:
        print("Unknown type")
    }
}
// Output:
// Int: 42
// String: hello
// Bool: true
// Double: 3.14
// Dog: Spot

AnyObject — Holds Any Class Instance

AnyObject holds instances of any class type only (not structs or enums).

var objects: [AnyObject] = [Dog(name: "Rex"), Cat(name: "Luna")]

for obj in objects {
    if let d = obj as? Dog { print("Dog: \(d.name)") }
    if let c = obj as? Cat { print("Cat: \(c.name)") }
}

Type Casting With Protocols

The same casting tools work when checking protocol conformance.

protocol Flyable {
    func fly()
}

class Bird: Animal, Flyable {
    func fly() { print("\(name) is flying!") }
}

let zoo: [Animal] = [Dog(name: "Rex"), Bird(name: "Tweety"), Cat(name: "Mimi")]

for creature in zoo {
    if let flyer = creature as? Flyable {
        flyer.fly()
    } else {
        print("\(creature.name) cannot fly.")
    }
}
// Output:
// Rex cannot fly.
// Tweety is flying!
// Mimi cannot fly.

type(of:) — Check Type at Runtime

Use type(of:) to get the actual runtime type of any value.

let value: Any = "Hello"
print(type(of: value))   // Output: String

let num: Any = 42
print(type(of: num))     // Output: Int

for animal in animals {
    print(type(of: animal))
}
// Output: Dog  Cat  Dog

Diagram: Casting Direction

Upcast (always safe, use as):
  Dog → Animal         guaranteed success

Downcast (may fail, use as?):
  Animal → Dog         returns Dog? (nil if it's actually a Cat)

Force downcast (risky, use as!):
  Animal → Dog         returns Dog (CRASH if it's a Cat)

Type check (use is):
  animal is Dog        returns true or false

Summary

Type casting lets you check (is), safely convert (as?), or force-convert (as!) between related types at runtime. Use Any to store mixed-type collections and switch with as patterns to handle each type. Always prefer as? over as! to avoid crashes. Type casting connects Swift's static type system to the dynamic needs of real apps.

Leave a Comment

Your email address will not be published. Required fields are marked *