Type placeholders in Swift
Discover page available: GenericsSwift’s type inference capabilities have been a very core part of the language since the very beginning, and heavily reduces the need for us to manually specify types when declaring variables and properties that have default values. For example, the expression var number = 7
doesn’t need to include any type annotations, since the compiler is able to infer that the value 7
is an Int
and that our number
variable should be typed accordingly.
Swift 5.6, which was released as part of Xcode 13.3, continues to expand these type inference capabilities by introducing the concept of “type placeholders”, which can come very much in handy when working with collections and other generic types.
For example, let’s say that we wanted to create an instance of Combine’s CurrentValueSubject
with a default integer value. An initial idea on how to do just that might be to simply pass our default value to that subject’s initializer, and then store the result in a local let
(just like when creating a plain Int
value). However, doing so gives us the following compiler error:
// Error: "Generic parameter 'Failure' could not be inferred"
let counterSubject = CurrentValueSubject(0)
That’s because CurrentValueSubject
is a generic type that needs to be specialized with not just an Output
type, but also with a Failure
type — which is the type of error that the subject is capable of throwing.
Since we don’t want our subject to throw any errors in this case, we’ll give it the failure type Never
(which is a common convention when using both Combine and Swift concurrency). But in order to do that, prior to Swift 5.6, we would need to explicitly specify our Int
output type as well — like this:
let counterSubject = CurrentValueSubject<Int, Never>(0)
Starting in Swift 5.6, though, that’s no longer the case — as we can now use a type placeholder for our subject’s output type, which lets us once again leverage the compiler to automatically infer that type for us, just like when declaring a plain Int
value:
let counterSubject = CurrentValueSubject<_, Never>(0)
That’s nice, but it’s arguably not the biggest improvement in the world. After all, we’re only saving two characters by replacing Int
with _
, and it’s not like manually specifying a simple type like Int
was something problematic to begin with.
But let’s now take a look at how this feature scales to more complex types, which is where it really starts to shine. For example, let’s say that our project contains the following function that lets us load a user-annotated PDF file:
func loadAnnotatedPDF(named: String) -> Resource<PDF<UserAnnotations>> {
...
}
The above function uses a quite complex generic as its return type, which might be because we’re reusing our Resource
type across multiple domains, and because we’ve opted to use phantom types to specify what kind of PDF that we’re currently dealing with.
Now let’s take a look at what our CurrentValueSubject
-based code from before would look like if we were to call the above function when creating our subject, rather than just using a simple integer:
// Before Swift 5.6:
let pdfSubject = CurrentValueSubject<Resource<PDF<UserAnnotations>>, Never>(
loadAnnotatedPDF(named: name)
)
// Swift 5.6:
let pdfSubject = CurrentValueSubject<_, Never>(
loadAnnotatedPDF(named: name)
)
That’s quite a huge improvement! Not only does the Swift 5.6-based version save us some typing, but since the type of pdfSubject
is now derived completely from the loadAnnotatedPDF
function, that’ll likely make iterating on that function (and its related code) much easier — since there will be fewer manual type annotations that will need to be updated if we ever change that function’s return type.
It’s worth pointing out, though, that there’s another way to leverage Swift’s type inference capabilities in situations like the one above — and that’s to use a type alias, rather than a type placeholder. For example, here’s how we could define an UnfailingValueSubject
type alias, which we’ll be able to use to easily create subjects that aren’t capable of throwing any errors:
typealias UnfailingValueSubject<T> = CurrentValueSubject<T, Never>
With the above in place, we’ll now be able to create our pdfSubject
without any kind of generic type annotations at all — since the compiler is able to infer what type that T
refers to, and since the failure type Never
has been hard-coded into our new type alias:
let pdfSubject = UnfailingValueSubject(loadAnnotatedPDF(named: name))
That doesn’t mean that type aliases are universally better than type placeholders, though, since if we were to define new type aliases for each particular situation, then that could also make our code base much more complicated. Sometimes, specifying everything inline (like when using a type placeholder) is definitely the way to go, as that lets us define expressions that are completely self-contained.
Before we wrap things up, let’s also take a look at how type placeholders can be used with collection literals — for example when creating a dictionary. Here, we’ve opted to manually specify our dictionary’s Key
type (in order to be able to use dot-syntax to refer to an enum’s various cases), while using a type placeholder for that dictionary’s values:
enum UserRole {
case local
case remote
}
let latestMessages: [UserRole: _] = [
.local: "",
.remote: ""
]
So that’s type placeholders — a new feature introduced in Swift 5.6, which will likely be really useful when dealing with slightly more complex generic types. It’s worth pointing out, though, that these placeholders can only be used at call sites, not when specifying the return types of either functions or computed properties.
I hope you found this article useful, and feel free to let me know if you have any questions, comments, or feedback — either via Twitter or email.
Thanks for reading!