Articles, podcasts and news about Swift development, by John Sundell.

Inferred generic type constraints

Published on 01 May 2020
Discover page available: Generics

Generic 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.