Weekly Swift articles, podcasts and tips by John Sundell.

Explicit type annotations

Published on 26 Mar 2020
Basics article available: Generics

One of Swift’s most important features is, without a doubt, its type inference engine. Without it, we’d have to always explicitly specify the type of each of our variables, closure arguments, and other expressions — which would dramatically increase the verbosity of our code.

However, even though Swift’s type inference engine is incredibly powerful, sometimes it does need a bit of a helping hand — and in those situations, being able to add explicit type annotations can be really useful.

As an example, let’s take a look at a Task type, which enables us to run closures in the background using the DispatchQueue API:

struct Task<Input, Output> {
    typealias Handler = (Result<Output, Error>) -> Void

    var body: (Input) throws -> Output

    func perform(with input: Input,
                 on queue: DispatchQueue = .global(),
                 then handler: @escaping Handler) {
        queue.async {
            handler(Result { try self.body(input) })
        }
    }
}

The above type is just an example. For a much more thorough implementation of a task-based concurrency system, see this article.

Since the above Task type is a generic (which is great in terms of type safety), the compiler will need to specialize each instance of it according to which Input and Output types are used at each given call site. While that’s exactly what the type inference engine can often take care of, completely automatically — sometimes our code might not contain enough type information for that to be possible.

For example, here we’re constructing a Task for loading a Config model over the network, by taking a URL as input and then returning a decoded instance of our model as output:

let loadConfigTask = Task { url in
    let data = try Data(contentsOf: url)
    let decoder = JSONDecoder()
    return try decoder.decode(Config.self, from: data)
}

If we try to compile the above code, the compiler will throw an error for both Input and Output, saying that neither of those two types could be inferred. So how can we fix this problem? One way is to explicitly type the Task instance itself, like this:

let loadConfigTask = Task<URL, Config> { url in
    ...
}

With the above change in place, no additional type information will be needed within our closure — since the compiler will be able to connect the Input and Output types to our closure’s parameter and return type. However, if we wanted to, we could’ve also opted to specify those closure types instead — like this:

let loadConfigTask = Task { (url: URL) -> Config in
    ...
}

Above we’re specifying both the input and output type of our closure, however, if we’re able to reduce its body to something that the compiler can more easily type-check (for example a single expression) — then we only have to specify the input type, and Swift’s type inference engine will take care of the rest:

let loadConfigTask = Task { (url: URL) in
    try JSONDecoder().decode(
        Config.self,
        from: Data(contentsOf: url)
    )
}

The above techniques can be great to keep in mind when dealing with generic code, since they often enable us to resolve many different kinds of ambiguous type information.