Swift is a statically typed language, which means that the type of every property, constant, and variable we declare needs to be specified at compile time. Typically, however, this is not something that has to be done manually; instead, the compiler is able to automatically infer various type information based on the assigned value — thanks to Swift’s support for type inference.
So, for example, here we declare a few constants – without specifying any type at all, because the compiler is able to infer the information based on the assigned values:
let number = 42
let string = “Hello, world!”
let array = [1, 1, 2, 3, 5, 8]
let dictionary = [“key”: “value”]
For comparison, here’s what it would look like if we manually specified the type of each constant:
let number: Int = 42
let string: String = “Hello, world!”
let array: [Int] = [1, 1, 2, 3, 5, 8]
let dictionary: [String: String] = [“key”: “value”]
Therefore, to make Swift syntax as lightweight as possible, type inference plays an important role, and type inference applies not only to variable declarations and assignment statements of other types, but in the case of many other types as well.
For example, here we define an enumeration that describes various contact types, and a function that loads Contact
an array of values belonging to a specific type:
enum ContactKind {
case family
case friend
case coworker
case acquaintance
}
func loadContacts(ofKind kind: ContactKind) -> [Contact] {
...
}
ContactKind.friend
While it is common to refer to members of the above enumeration by specifying both the type and the member (eg When calling the above function:
let friends = loadContacts(ofKind: .friend)
What’s really cool is that the above “dot syntax” doesn’t just apply to enumeration members, but also when referencing any static property or method. For example, here we extend the Foundation URL
class and add a static attribute to create a link to this website URL
:
extension URL {
static var swiftBySundell: URL {
URL(string: “https://swiftbysundell.com”)!
}
}
Now, when calling any URL
method that accepts a parameter (such as the new Combine framework enhanced URLSession
API), we can simply reference the above property:
let publisher = URLSession.shared.dataTaskPublisher(for: .swiftBySundell)
Very cool! However, although type inference is a very useful feature, there are still some situations where it may be necessary to specify additional type information to achieve the desired results.
A very common example of these situations is dealing with numeric types. When a numeric literal is assigned to a variable or constant, it will be inferred to be of Int
type by default – which is a perfectly reasonable default – but if you wish to use other numeric types, such as Double
or Float
, you will need to specify these types manually. Here are a few ways:
let int = 42
let double = 42 as Double
let float: Float = 42
let cgFloat = CGFloat(42)
Another situation is that when calling a function with a generic return type , you may also need to provide additional type information to the compiler.
For example, here we extend the built-in Bundle
class, adding a generic method that can easily load and decode any JSON file bundled in the application:
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 (in the above code, through the use of the
throws
andtry
keywords), check out the basic article on error handling .
Now imagine that in application development, before the real server and network code is ready, we want to decode User
instances of the following types from the bundled JSON file:
struct User: Codable {
var name: String
var email: String
var lastLoginDate: Date
}
However, if you call the method like this decodeJSONFile
, you will get a compiler error:
//
let user = try Bundle.main.decodeJSONFile(named: “user-mock”)
This is because the exact type of any given JSON file that is about to be decoded depends on what the generic type T actually references at each call point – and since we didn’t provide the compiler with any such information above, it will Got an error. In this case, the compiler has no way of knowing what type of User
instance we wish to decode.
To solve this problem, we can use the same technique as above for specifying different types of values, either giving our user
constant an explicit type or using as
keywords – 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 called in a context where the expected return type is known decodeJSONFile
, it is possible again to let Swift’s type inference mechanism find that information – as in the following case, where we define a MockData
wrapper struct called , this structure has an User
attribute of type, and we assign the result to this attribute:
struct MockData {
var user: User
}
let mockData = try MockData(
user: Bundle.main.decodeJSONFile(named: “user-mock”)
)
That’s a brief introduction to Swift’s type inference capabilities. It’s worth pointing out that type inference does have a computational cost associated with it, but fortunately these costs occur entirely at compile time (and therefore do not affect the runtime performance of the application), but when dealing with more complex expressions , still worth noting. If we encounter an expression that takes the compiler a long time to figure out, we can always specify the types manually using any of the techniques above.