Inferred generic type constraints
Discover page available: GenericsGeneric type constraints can be used to impose a set of requirements on the concrete types that a given API will be used with, which in turn enables us make certain assumptions about those types within our generic code.
For example, let’s say that an app that we’re working on is using a NetworkRequest
protocol to enable us to define our various requests as different types that each declare what kind of Response
that they expect:
protocol NetworkRequest {
associatedtype Response: Codable
func makeURLRequest() -> URLRequest
}
If we now define an API for performing such a request, we might end up with something like the following — a function that uses a generic type constraint to ensure that its T
type does in fact conform to our above protocol:
func perform<T: NetworkRequest>(
_ request: T,
then handler: @escaping (Result<T.Response, Error>) -> Void
) {
...
}
Now let’s say that we also want to add support for enqueuing requests using a NetworkRequestQueue
class — that also uses a generic type constrained to our NetworkRequest
protocol:
class NetworkRequestQueue<Request: NetworkRequest> {
...
}
When it comes to adding a parameter for injecting an instance of the above class into our perform
function, we might initially simply add it to our list of parameters — like this:
func perform<T: NetworkRequest>(
_ request: T,
on queue: NetworkRequestQueue<T>,
then handler: @escaping (Result<T.Response, Error>) -> Void
) {
...
}
However, since our NetworkRequestQueue
class already requires its generic Request
type to conform to our NetworkRequest
protocol — and since the compiler knows that the type used to specialize that class will be the same as the one used for our request
parameter — we can actually omit that type constraint from our perform
function entirely, like this:
func perform<T>(
_ request: T,
on queue: NetworkRequestQueue<T>,
then handler: @escaping (Result<T.Response, Error>) -> Void
) {
...
}
The above might seem like a small detail, and perhaps it is in the grand scheme of things — but since generic code tends to become quite verbose, especially when using type constraints, anything we can do to reduce that sort of verbosity is arguably a good thing.
As an added bonus, let’s also make the above handler
parameter’s closure type a bit simpler as well — by first turning it into a type alias within an extension on our NetworkRequest
protocol:
extension NetworkRequest {
typealias ResponseHandler = (Result<Response, Error>) -> Void
}
With the above in place, we can now make our perform
function’s signature even nicer to read — like this:
func perform<T>(
_ request: T,
on queue: NetworkRequestQueue<T>,
then handler: @escaping T.ResponseHandler
) {
...
}
Swift’s syntax might initially seem quite verbose when it comes to defining generic types and functions, but there are often certain tricks and techniques that we can use to make such definitions much more compact — without sacrificing any readability.