Weekly Swift articles, podcasts and tips by John Sundell.

Type inference

Published on 08 Jul 2020

Swift is a statically typed language, meaning that the type of every property, constant and variable that we declare needs to be specified at compile time. However, that’s often not something that needs to be done manually, instead the compiler is able to figure out a wide range of type information on its own — thanks to the fact that Swift supports type inference.

So for example, here we’ve declared a few constants — all without specifying any types at all, since the compiler is able to infer that information based on the values that are being assigned:

let number = 42
let string = "Hello, world!"
let array = [1, 1, 2, 3, 5, 8]
let dictionary = ["key": "value"]

For comparison, here’s what the above assignments would look like if we instead were to manually specify the types of each of our constants:

let number: Int = 42
let string: String = "Hello, world!"
let array: [Int] = [1, 1, 2, 3, 5, 8]
let dictionary: [String: String] = ["key": "value"]

So type inference plays a major part in making Swift’s syntax as lightweight as possible, not only when it comes to variable declarations and other kinds of assignments, but in many other kinds of situations as well.

For example, here we’ve defined an enum that describes various kinds of contacts, and a function that lets us load an array of Contact values that belong to a certain kind:

enum ContactKind {
    case family
    case friend
    case coworker
    case acquaintance
}

func loadContacts(ofKind kind: ContactKind) -> [Contact] {
    ...
}

While we would normally refer to a member of the above enum by specifying both the type and the case (such as ContactKind.friend), thanks to type inference, we can completely omit the name of the type when referring to a case within a context in which the type is known — like when calling our above function:

let friends = loadContacts(ofKind: .friend)

What’s really cool is that the above “dot syntax” doesn’t just work for enum cases, it works when referencing any static property or method as well. For example, here we’ve extended Foundation’s URL type with a static property that creates a URL that points to this very website:

extension URL {
    static var swiftBySundell: URL {
        URL(string: "https://swiftbysundell.com")!
    }
}

Now, when calling any method that accepts a URL parameter (for example the new Combine-powered URLSession API), we can simply refer to the above property like this:

let publisher = URLSession.shared.dataTaskPublisher(for: .swiftBySundell)

Really neat! However, while type inference is an incredibly useful feature, there are still situations in which we might need to specify a bit of extra type information in order to achieve our desired result.

A very common example of such a situation is when dealing with numeric types. When a numeric literal is assigned to a variable or constant, it’ll by default be inferred to have the type Int — which is a completely reasonable default — but if we wish to use another numeric type, such as Double or Float, we’ll need to specify those types manually. Here are a few ways to do that:

let int = 42
let double = 42 as Double
let float: Float = 42
let cgFloat = CGFloat(42)

Another type of situation in which we might need to give the compiler some additional type information is when calling a function that has a generic return type.

For example, here we’ve extended the built-in Bundle type with a generic method that lets us easily load and decode any JSON file that we’ve bundled within our app:

extension Bundle {
    struct MissingFileError: Error {
        var name: String
    }

    func decodeJSONFile<T: Decodable>(named name: String) throws -> T {
        guard let url = self.url(forResource: name, withExtension: "json") else {
            throw MissingFileError(name: name)
        }

        let data = try Data(contentsOf: url)
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    }
}

To learn more about Swift’s built-in error handling mechanism (that we use above through the throws and try keywords), check out the Basics article about error handling.

Now let’s say that during the development of our app, until our real server and networking code is up and running, we wish to decode an instance of the following User type from a bundled JSON file:

struct User: Codable {
    var name: String
    var email: String
    var lastLoginDate: Date
}

However, if we were to call our decodeJSONFile method like this, we’d end up with a compiler error:

// Error: Generic parameter 'T' could not be inferred
let user = try Bundle.main.decodeJSONFile(named: "user-mock")

That’s because the exact type that we’ll decode any given JSON file into depends on what the generic type T will actually refer to at each call site — and since we’re not giving the compiler any such information above, we’ll end up with an error. There’s simply no way for the compiler to know that we wish to decode a User instance in this case.

To fix that problem, we can use the same sort of techniques that we used above to specify different kinds of numeric values, and either give our user constant an explicit type, or use the as keyword — like this:

let user: User = try Bundle.main.decodeJSONFile(named: "user-mock")
let user = try Bundle.main.decodeJSONFile(named: "user-mock") as User

However, if we were to call our decodeJSONFile method within a context in which our desired return type is known, then we could once again let Swift’s type inference mechanism figure that information out for us — like in this case, in which we’ve defined a wrapper struct called MockData that has a User-typed property that we’re assigning our result to:

struct MockData {
    var user: User
}

let mockData = try MockData(
    user: Bundle.main.decodeJSONFile(named: "user-mock")
)

So that’s a quick intro to Swift’s type inference capabilities. It’s also important to point out that type inference does have a computation cost associated with it, which thankfully takes place entirely at compile-time (so it won’t effect the runtime performance of our apps), but it’s still something that can be good to keep in mind when dealing with more complex expressions. However, if we do encounter an expression that takes the compiler a long time to figure out, then we could always use one of the above techniques for specifying those types manually.

Thanks for reading! 🚀