Type checking vs type casting in Swift

Updated Jan 13, 2024#swift#types

Swift is a statically typed language, which means that every variable and expression in a program has a specific type. Swift provides several ways to check and cast types, which can be useful when working with objects that have been inherited from a superclass or implemented a protocol.

Type checking is typically done at compile time, generally safer as it doesn’t modify the underlying instance, and useful for conditional logic based on types.

Type casting is done at runtime, can lead to runtime errors if not used carefully, and useful for accessing specific methods or properties of a subclass when you have a reference to a superclass instance.

Type checking using is operator

The is operator in Swift is used to check whether an instance belongs to a certain type or conforms to a certain protocol. It returns a boolean value that indicates whether the instance is of the specified type or conforms to the specified protocol.

class Animal {}
class Dog: Animal {}
class Cat: Animal {}

let animal: Animal = Dog()

if animal is Dog {
    print("This animal is a dog")
} else if animal is Cat {
    print("This animal is a cat")
} else {
    print("This animal is neither a dog nor a cat")
}

// Prints: This animal is a dog

We could also use the is operator to check whether an instance conforms to a protocol. Here’s an example:

protocol CanFly {}
class Bird: CanFly {}
class Plane {}

let objects: [Any] = [Bird(), Plane()]

for object in objects {
    if object is CanFly {
        print("This object can fly")
    } else {
        print("This object cannot fly")
    }
}

// Prints:
// This object can fly
// This object cannot fly

Dynamic type checking using type(of:)

You can use the type(of:) function to find the dynamic type of a value, particularly when the dynamic type is different from the static type. The static type of a value is the known, compile-time type of the value. The dynamic type of a value is the value’s actual type at run-time, which can be a subtype of its concrete type.

let values: [Any] = [1, "two", 3.0]

for value in values {
    let type = type(of: value)
    print("\(value) is of type \(type)")
}

// Prints:
// 1 is of type Int
// two is of type String
// 3.0 is of type Double

Here’s an example that combines the use of type(of:) with the is operator to perform type checking on values of unknown type, which can be very useful in situations where we need to handle different types of values differently.

let values: [Any] = [1, "two", 3.0]

for value in values {
    let type = type(of: value)
    
    if type is Int.Type {
        print("\(value) is an integer")
    } else if type is String.Type {
        print("\(value) is a string")
    } else if type is Double.Type {
        print("\(value) is a double")
    } else {
        print("\(value) is of an unknown type")
    }
}

// Prints:
// 1 is an integer
// two is a string
// 3.0 is a double

The dynamic type returned from type(of:) is a concrete metatype (T.Type) for a class, structure, enumeration, or other nonprotocol type T, or an existential metatype (P.Type) for a protocol or protocol composition P. When the static type of the value passed to type(of:) is constrained to a class or protocol, you can use that metatype to access initializers or other static members of the class or protocol.

class Smiley {
    class var text: String {
        return ":)"
    }
}

class EmojiSmiley: Smiley {
     override class var text: String {
        return "😀"
    }
}

func printSmileyInfo(_ value: Smiley) {
    let smileyType = type(of: value)
    print("Smile!", smileyType.text)
}

let emojiSmiley = EmojiSmiley()
printSmileyInfo(emojiSmiley)
// Smile! 😀

When working with generic types, it can be useful to know the dynamic type of a generic value at runtime. type(of:) can be used to extract the dynamic type of a generic value and use it to perform specific operations or type checking.

func printType<T>(_ value: T) {
    print(type(of: value))
}

printType("Hello, world!") // Output: "String"
printType(42) // Output: "Int"

When working with protocol composition, type(of:) can be used to extract the dynamic type of an object that conforms to multiple protocols.

protocol MyProtocol1 {}
protocol MyProtocol2 {}
class MyClass: MyProtocol1, MyProtocol2 {}

let myObject: MyProtocol1 & MyProtocol2 = MyClass()
print(type(of: myObject)) // Output: "MyClass"

Type casting using as, as?, and as!

Type casting includes upcasting and downcasting, which are two related concepts in Swift that involve converting between types in an inheritance hierarchy.

You can try to downcast to the subclass type with a type cast operator (as? or as!). Casting doesn’t actually modify the instance or change its values. The underlying instance remains the same; it’s simply treated and accessed as an instance of the type to which it has been cast.

Because downcasting can fail, the type cast operator comes in two different forms. The conditional form, as?, returns an optional value of the type you are trying to downcast to. The forced form, as!, attempts the downcast and force-unwraps the result as a single compound action.

class Animal {
    func makeSound() {
        print("Unknown sound")
    }
}

class Dog: Animal {
    override func makeSound() {
        print("Bark")
    }
}

let myAnimal = Animal()
let myDog = myAnimal as? Dog // downcasting

myAnimal.makeSound() // Output: "Unknown sound"
myDog?.makeSound() // Output: nothing

Upcasting refers to converting an instance of a subclass to its superclass type. This is done implicitly by the Swift compiler, and can also be done explicitly using the as keyword. Upcasting is safe, because all instances of a subclass can be treated as instances of its superclass.

class Animal {
    func makeSound() {
        print("Unknown sound")
    }
}

class Dog: Animal {
    override func makeSound() {
        print("Bark")
    }
}

let myDog = Dog()
let myAnimal = myDog as Animal // upcasting

myDog.makeSound() // Output: "Bark"
myAnimal.makeSound() // Output: "Bark"

The Any type represents values of any type, including optional types. Swift gives you a warning if you use an optional value where a value of type Any is expected. If you really do need to use an optional value as an Any value, you can use the as operator to explicitly cast the optional to Any, as shown below.

var things: [Any] = []

things.append(0)
things.append(0.0)
things.append("hello")

let optionalNumber: Int? = 3
things.append(optionalNumber)        // Warning
things.append(optionalNumber as Any) // No warning