Explicit type annotations
Basics article available: Type inferenceOne 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.