• Skip to main content
  • Skip to footer

Khawer Khaliq

  • Home

What Are Swift Optionals and How They Are Used

Share
Tweet
Share
Pin

Optional is the mechanism in Swift to indicate the possible absence of a value or a reference to an object. While it can take a bit of getting used to for those unfamiliar with this concept, appropriate use of optionals can make Swift code safer and more expressive. This article explains what optionals are, why Swift has them, and the ways in which optionals can be used in Swift to tackle otherwise difficult situations in an elegant and effective manner.

Related articles:

  • The Power of Optional Chaining in Swift
  • Unwrapping Optionals With Optional Binding in Swift

Contents

What is an optional in Swift
Why does Swift have optionals
How are optionals implemented in Swift
When to use optionals in Swift
1. Property not guaranteed to have a value
2. Function not guaranteed to return a value
3. Initializer not guaranteed to create an instance
4. Function parameter not required in every case
5. Conditional type casting
6. Simplified error handling
Conclusion

What is an optional in Swift

Optional is a special Swift type, instances of which may or may not contain an instance of a given Swift type. An optional can be thought of as a box, which may be empty or may contain (wrap in Swift parlance) an instance of a given Swift type. It is noteworthy that while optionals can be used to wrap instances of any Swift type, including the Optional type, when an instance of Optional is created, the type that it can wrap must be specified. Once created, an Optional instance can be in one of two states – it either wraps a single instance of the specified wrapped type or it is empty. An empty optional is equivalent to nil, which is a special literal representing the absence of a value.

Optionals are such an important part of Swift that a special syntax has been baked into the language to make them easy to use. We can declare an optional wrapping any Swift type by simply postfixing the name of the type to be wrapped with a question mark (?). This works not only for types in the Standard Library, such as Int and String, but also for user-defined types, including reference types (classes and closures) and values types (structs, enums and tuples).

As an example, we declare an optional with String as the wrapped type, and give it a String value to wrap.

var maybeString: String? = "My string" 

We can use the type(of:) function from the Standard Library to check the type of maybeString and use an equality check to confirm that it is not equal to nil, since it was initialized with a wrapped value.

print(type(of: maybeString))    // Optional<String>
print(maybeString == nil)       // false

The assignment statement above seems to break the strict typing rules of Swift, since we are able to assign a String value to a variable of type Optional<String>. This is one of the many syntactic conveniences built into Swift to make optionals easier to use. When we assign an instance of the wrapped type to an optional variable, behind the scenes, an optional wrapping the given instance is created and assigned to the variable. The same automatic wrapping also works when calling functions, which means that if we have a function with an optional as a parameter, we can pass as an argument a non-optional instance of the type wrapped by the optional. It is important to note that the reverse is not true. If we try to assign an optional to a variable of the wrapped type or pass an optional to a function that expects an instance of the wrapped type, we will get an error.

In the spirit of syntactic convenience, Swift also allows us to create an optional without providing an instance of the wrapped type, as shown below.

var maybeInt: Int?

If we don’t give an optional variable an initial value when we declare it, an empty optional, which equates to nil, is automatically created and assigned to the variable. This is the only case where Swift automatically initializes a variable. We can confirm this by doing a type and equality check on maybeInt.

print(type(of: maybeInt))    // Optional<Int>
print(maybeInt == nil)       // true

An empty optional can be assigned any instance of the wrapped type. We can see this in action by assigning a value to maybeInt, after which it is no longer equal to nil.

maybeInt = 5
print(maybeInt == nil)       // false

Note that although optionals wrapping String and Int instances are actually of types Optional<String> and Optional<Int> respectively, as we have seen above, we can use the shorthand String? and Int?, not only to declare such optionals but also for all purposes thereafter.

Why does Swift have optionals

This is a question normally asked by programmers coming to Swift from languages that don’t have the concept of optionals. There are times when we need some way to signal in our code that a value or a reference to an object may or may not exist. At other times, we would like to be assured that when we expect to have a value or a reference to an object, it will be there. In languages that don’t have built-in support for optionals, where such guarantees are not built into the language, it can lead to undesirable patterns in code, such as unnecessary checking to ensure that an object reference points to an actual object or, when dealing with scalar types, using sentinel values, which are arbitrary values assumed to have a special meaning in a given context, such as a function meant to return the index of an array returning -1 when there is no valid index value to return.

Swift resolves these problems from the ground up. First, Swift does not have any scalar types. All types in Swift are what may loosely be called object types, which means they can be instantiated and messages can be sent to the instances thus created. This is true for all value and reference types in Swift. Second, Swift optionals formalize the distinction between situations where a value or a reference must be available and those where a value or a reference legitimately may or may not be present.

A non-optional variable in Swift must always have a value or a reference, as the case may be, and cannot be used without being initialized. Similarly, a Swift function whose return type is non-optional will not compile unless it returns a value or a reference, depending on whether the return type is a value or a reference type. This provides an iron-clad guarantee that if the type of a variable or the return type of a function has not been declared to be optional, it will always have a value or a reference. On the other hand, if the type of a variable or the return type of a function is declared as optional, any code that uses such a variable or function must use special syntax built into the language not only to check whether a value or a reference actually exists but also to use it safely if it does.

How are optionals implemented in Swift

We have already noted that an optional is a wrapper that can wrap an instance of a given Swift type. This is implemented using a generic enum as follows.

public enum Optional<Wrapped> {
    case none
    case some(Wrapped)
}

The enum has two cases, to represent the absence and presence of a wrapped instance respectively. When there is no wrapped instance, the value of the optional is .none. When the optional has a wrapped instance, its value is .some, with the wrapped instance as the associated value. Since Optional is a generic type, the Wrapped type parameter is used to determine the type of the wrapped instance at the time an Optional instance is created.

This explains why optionals wrapping a String and an Int are of types Optional<String> and Optional<Int> respectively. Although we could declare an optional using this syntax, it is much more convenient and customary to use the String? and Int? shorthand type names. The nil literal serves as a convenient shorthand for the case .none.

An optional can be initialized with a nil literal because the Optional type conforms to the protocol ExpressibleByNilLiteral, which has the following requirement.

protocol ExpressibleByNilLiteral {
    init(nilLiteral: ())
}

The type of the parameter nilLiteral is an empty tuple, which signifies the lack of a value. When this initializer is called, it creates a new Optional instance with the value .none. While this protocol is not specific to Optional, no other Swift type conforms to this protocol and use of this protocol with other types is discouraged to avoid confusion with Optional. This initializer should not be called directly. It is automatically called by the compiler when an optional is initialized using the nil literal (or an optional is created without providing an instance of the wrapped type, which has the same effect).

When to use optionals in Swift

Optionality is a common feature of real-world domains. By formalizing the concept of optionality, Swift can help us create better domain models and write code that can deal explicitly with cases where a certain relationship or association may or may not be present for a particular instance, either intrinsically or at a point in time.

It is also common to encounter situations in our code when a certain outcome cannot be guaranteed. Functions may be unable to return a value in all cases, creation of new instances of certain types may fail under certain conditions, not all type casting operations may succeed, etc. Optionals provide a simple and elegant way to deal with such situations where the reason for the failure can be discerned from the context.

Let us explore this subject in greater detail and look at some of the common scenarios where optionals are used in Swift.

1. Property not guaranteed to have a value

It is not uncommon to come across properties that may have a value for some instances of the type but not for others. Similarly, there could be properties that may have a value at certain times but not at others. Optionals are the natural choice to formally express such constraints. To illustrate, let us define a protocol to model pets and a class to model people who may own these pets.

protocol Pet {
    var name: String? { get set }
    func makeSound()
}

class Person {
    var name: String
    var pet: Pet?
    
    init(named name: String) {
        self.name = name
    }
}

The Pet protocol declares a method which conforming types will implement to indicate the sound that each pet makes. It also declares a property for the name of the pet. The Person class has two properties, one for the name of the person and the other for the pet the person may have.

Since not every person may have a pet, and someone who has a pet once is not guaranteed to always have one, the pet property in Person is an optional. Also, while the name property in Person is non-optional, the name property in Pet is declared as an optional. This is to reflect the requirement that, while every person must have a name, not every pet is required to have a name.

2. Function not guaranteed to return a value

There could be functions, methods or closures with declared return types that may not be able to return a value in all cases. Usually this indicates that the operation could not be performed as requested.

As an example, the Collection protocol in the Standard Library defines a firstIndex(of:) method, available when elements of the collection conform to Equatable, which returns the first index where the specified value appears in the collection. It is possible, however, that the value does not appear in the collection. That is why the return type of the method is Int?. If the value is found, its integer index is returned wrapped in an optional; if not, nil is returned.

To demonstrate this, we define a simple generic function which looks for the first index of a given element in an array, where elements of the array conform to Equatable, printing the first index if the element is found and printing a suitable message if it isn’t.

func printFirstIndex<Element: Equatable>(of element: Element, in array: [Element]) {
    let maybeIndex = array.firstIndex(of: element)
    if let index = maybeIndex {
        print("Element \(element) was found at index \(index)")
    } else {
        print("Element \(element) not found")
    }
}

Here is how we can use the above function with an array of integers.

let ints = [2, 3, 5]
printFirstIndex(of: 3, in: ints)    // Element 3 was found at index 1
printFirstIndex(of: 7, in: ints)    // Element 7 not found

3. Initializer not guaranteed to create an instance

There could be circumstances under which an initializer of a type may not be able to create a valid instance. This could be due to some or all arguments not being valid or some other required condition not being met. To cater to such situations, Swift lets us declare a failable initializer, which returns an optional wrapping the newly created instance. If instance creation fails, nil is returned. A failable initializer is declared by putting a question mark (?) before the opening parenthesis of the parameter list.

Looking at the Person class defined above, the name property of Person is not an optional since every person must have a name. But we could still create a nameless person by passing an empty string as the argument.

let namelessPerson = Person(named: "")

To prevent this from happening, we can modify the Person class to make the initializer failable.

class Person {
    var name: String
    var pet: Pet?
    
    init?(named name: String) {
        guard !name.isEmpty else { return nil }
        self.name = name
    }
}

Now a Person instance with an empty string for a name cannot be initialized.

let notAPerson = Person(named: "")
print(type(of: notAPerson))     // Optional<Person>
print(notAPerson == nil)        // true

4. Function parameter not required in every case

A function may not need all its parameters for every invocation. Optional parameters provide an elegant way to handle such cases. When using such a function, for each optional parameter, the caller must either pass a value of the type wrapped by the optional or use nil. Alternatively, an optional parameter can be given a default of nil, which gives the caller the option of calling the function with as many arguments as may make sense for a particular invocation. This also applies to initializers.

Continuing our running example, we define a Cat class, which conforms to the Pet protocol.

class Cat: Pet {
    var name: String?

    init(named name: String? = nil) {
        self.name = name
    }
    
    func makeSound() {
        print("Meow")
    }
}

We have defined an initializer with a parameter of type String?, with a default value of nil. We can use this initializer to create Cat instances with or without a name.

let unnamedCat = Cat()
let namedCat = Cat(named: "Bella")

5. Conditional type casting

When we cast from a subtype to a supertype, the cast is guaranteed to succeed. The same is true when casting an instance of a concrete type to the type of a protocol to which the concrete type conforms. For casts that are guaranteed to succeed, we use the as operator.

var cat = Cat()
var somePet: Pet = cat as Pet

However, when we cast from a supertype to a subtype or from a protocol type to a concrete type, we cannot use the as operator since such casts are not guaranteed to succeed. We demonstrate this using the somePet and cat variables declared above.

somePet = Cat()
cat = somePet as Cat
// Error: 'Pet' is not convertible to 'Cat'

Although the instance we are trying to cast is of class Cat, we get a compiler error. This is because the variable somePet is typed as Pet and the compiler cannot be certain that the runtime type of the instance we are trying to cast will always be one for which the cast will succeed.

For casts that are not guaranteed to succeed, we can post-fix the as operator with a question mark (?). If the cast succeeds, we get the result of the cast wrapped in an optional.

var maybeCat = somePet as? Cat
print(type(of: maybeCat))    // Optional<Cat>
print(maybeCat == nil)       // false

If a conditional cast fails, we get nil. To demonstrate this, we define a Dog class that also conforms to Pet.

class Dog: Pet {
    var name: String?

    init(named name: String? = nil) {
        self.name = name
    }
    
    func makeSound() {
        print("Woof")
    }
}

somePet = Dog()
maybeCat = somePet as? Cat
print(maybeCat == nil)       // true

Since we have assigned an instance of Dog to somePet, the cast to Cat fails and maybeCat equals nil.

6. Simplified error handling

Optionals provide a convenient means for handling simple errors where the reason for the error can be easily discerned from the context. For more complex error handling requirements, we would normally use the error handling mechanism built into Swift. The usual way to handle errors in Swift is to put any statement or function call that can throw an error in a try expression. If an error is thrown, it must be handled in some way, either by catching it with a do-catch statement or propagating it by marking the function as throwing.

However, there may be situations when a particular use case only requires knowing whether an error got thrown and not which error it was. This can come in handy when using frameworks and third-party libraries where we may not be interested in the details of some of the errors thrown but only whether an operation failed.

To do this, we can post-fix try with a question mark (?). A try? expression does not need to appear in a do-catch statement or a throwing function. If no error is thrown, we get the result obtained from evaluating the expression wrapped in an optional. Otherwise, we get nil.

We demonstrate this by defining an error type with a single error case. We also define a throwing function, which takes a single boolean parameter to help us control whether an error gets thrown.

enum TestError: Error {
    case someError
}

func possibleThrower(shouldThrow: Bool) throws -> String {
    if shouldThrow {
        throw TestError.someError
    }
    return "My string"
}

We can use the above function to see the result of a try? expression, both when the throwing function actually throws an error and when it does not.

var stringResult = try? possibleThrower(shouldThrow: false)
print(type(of: stringResult))   // Optional<String>
print(stringResult == nil)      // false

stringResult = try? possibleThrower(shouldThrow: true)
print(stringResult == nil)      // true

When the function in the try? expression does not throw an error, we get an optional wrapping the return value of the function. When an error is thrown, we get nil to indicate failure.

This works for functions with a return type. But how about functions with no return type? Interestingly, in Swift, there is no such thing as a function with no return type. Functions with no return type implicitly have a return type of Void and a special return value of (), which is an empty tuple. An optional can be used to wrap any Swift type, including Void.

We can use this to extend our above example to a function without an explicit return type.

func anotherPossibleThrower(shouldThrow: Bool) throws {
    if shouldThrow {
        throw TestError.someError
    }
}

var voidResult: Void? = try? anotherPossibleThrower(shouldThrow: false)
print(voidResult == nil)    // false

voidResult = try? anotherPossibleThrower(shouldThrow: true)
print(voidResult == nil)    // true

The function anotherPossibleThrower has an implicit return type of Void which becomes Void? when we use it in a try? expression. We can capture the implicit return value to check whether it is nil, which means an error was thrown. Note that we have to explicitly declare the type of the variable voidResult to avoid getting a warning from the compiler for attempting to capture a return value that we have not explicitly declared.

Conclusion

Optionals can impose a bit of a learning curve on programmers new to Swift and can feel frustrating at times. Once mastered though, optionals feel natural in the way they can make code safer and more expressive by providing a way to indicate the possible absence of a value or a reference to an object.

Thank you for reading! I always appreciate constructive comments and feedback. Please feel free to leave a comment in the comments section of this post or start a conversation on Twitter.

Subscribe to get notifications of new posts

No spam. Unsubscribe any time.

Reader Interactions

Leave a Reply Cancel reply

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

Footer

Protocol-Oriented Programming (POP) in Swift

Use protocol-oriented programming to think about abstractions in a completely different way, leveraging retroactive modeling to introduce appropriate abstractions at any point in the development cycle, and creating traits that can let types opt into functionality simply by conforming to a protocol.

Pattern Matching With Optionals in Swift

The optional pattern explained in detail, including how to use it with a variety of conditional statements and loops, and how to add extra conditions when required, with a section on creating more complex pattern matching code involving optionals.

Test-Driven Development (TDD) in Swift

Learn how to use Test-Driven Development (TDD) in Swift which not only enables you to write more reliable and maintainable code but also allows refactoring of code in small increments and with greater ease and confidence.

Unwrapping Optionals With Optional Binding in Swift

Learn how to use optional binding to extract the value wrapped by an optional to a constant or variable, as part of a conditional statement or loop, exploring where optional chaining may be used in place of optional binding, and where these techniques can be used together.

Unit Testing and UI Testing in Swift

Learn how to use unit testing to gain confidence in the correctness of code at the unit level, and use UI testing to ensure that the application fulfills user requirements, explained in detail and illustrated using an example application built using SwiftUI.

When and How to Use the Equatable and Identifiable Protocols in Swift

Detailed coverage of the Equatable and Identifiable protocols, and how they can be used to model not only values but also domain entities with identity using Swift value types, to create code that is more efficient, easier to reason about, easily testable and more concurrency-friendly.

The Power of Optional Chaining in Swift

Learn how to use optional chaining to work safely with optionals, to set and retrieve the value of a property of the wrapped instance, set and retrieve a value from a subscript on the wrapped instance, and call a method on the wrapped instance, all without having to unwrap the optional.

A Protocol-Oriented Approach to Associated Types and Self Requirements in Swift

Use protocol-oriented programming to avoid having to use associated types in many situations but also to effectively use associated types and Self requirements, where appropriate, to leverage their benefits while avoiding the pitfalls.

Encapsulating Domain Data, Logic and Business Rules With Value Types in Swift

Leverage the power of Swift value types to manage domain complexity by creating rich domain-specific value types to encapsulate domain data, logic and business rules, keeping classes lean and focused on maintaining the identity of entities and managing state changes through their life cycles.

Rethinking Design Patterns in Swift – State Pattern

The State pattern, made simpler and more flexible with the power of Swift, with a detailed worked example to illustrate handling of new requirements, also looking at key design and implementation considerations and the benefits and practical applications of the pattern.

Conditional Logic With and Without Conditional Statements in Swift

Shows how to implement conditional logic using conditional statements as well as data structures, types and flow control mechanisms such as loops, balancing simplicity and clarity with flexibility and future-proofing.

Better Generic Types in Swift With the Numeric Protocol

Covers use of the Numeric protocol as a constraint on type parameters of generic types to ensure that certain type parameters can only be used with numeric types, using protocol composition to add relevant functionality as required.

Understanding, Preventing and Handling Errors in Swift

Examines the likely sources of errors in an application, some ways to prevent errors from occurring and how to implement error handling, using the error handling model built into Swift, covering the powerful tools, associated techniques and how to apply them in practice to build robust and resilient applications.

When and How to Use Value and Reference Types in Swift

Explores the semantic differences between value and reference types, some of the defining characteristics of values and key benefits of using value types in Swift, leading into a discussion on how value and reference types play a complementary role in modeling real-world domains and designing applications.

Swift Protocols Don’t Play Nice With Equatable. Or Can They? (Part Two)

Uses type erasure to implement Equatable conformance at the protocol level, allowing us to program to abstractions using protocol types while safely making equality comparisons and using functionality provided by the Swift Standard Library only available to types that conform to Equatable.

Copyright © Khawer Khaliq, 2017-25. All rights reserved.