Swift 5.7: Primary Associated Types and Opaque Return Types

KD Knowledge Diet
3 min readMar 25, 2024

Ever since Swift’s inception, working with generic protocols, especially those involving Self requirements or associated types, often required type erasure to achieve flexibility. This need for type erasure became particularly prominent when using frameworks like Combine and SwiftUI.

The Era of Type Erasure

In earlier versions of Swift, when using Apple’s Combine framework for reactive programming, returning a `Publisher` from a function or computed property necessitated type erasure by wrapping it within an `AnyPublisher`. Here’s an example:

struct UserLoader {
func loadUser(withID id: User.ID) -> AnyPublisher<User, Error> {
// … data loading and decoding
}
}

This type erasure was necessary because simply declaring that a method returns something conforming to the `Publisher` protocol didn’t provide enough information to the compiler about the publisher’s output and error types.

Opaque Return Types in SwiftUI

Swift 5.1 introduced the `some` keyword and opaque return types, which SwiftUI adopted extensively. Opaque return types allow the compiler to infer the concrete type returned from a view’s body. For instance:

struct ArticleView: View {
var article: Article
var body: some View {
// SwiftUI code
}
}

While `some` worked well in SwiftUI, it had limitations when defining APIs for general use. Replacing `AnyPublisher` with `some Publisher` would technically work but would make it impossible for call sites to know the type of output that the publisher emits.

struct UserLoader {
func loadUser(withID id: User.ID) -> some Publisher {
// … data loading and decoding
}
}

Call sites would deal with an opaque `Publisher` type and couldn’t access the protocol’s associated types.

Primary Associated Types to the Rescue

Swift 5.7 introduces primary associated types, enhancing the `Publisher` protocol by declaring its associated `Output` and `Failure` types as primary using angle brackets right after the protocol’s name:

protocol Publisher<Output, Failure> {
associatedtype Output
associatedtype Failure: Error
// … other protocol requirements
}

With this update, you can use the `some` keyword in a new way, specifying the exact types for each primary associated type when declaring the return value. This eliminates the need for type erasure and maintains type safety from the method declaration to its call sites:

struct UserLoader {
func loadUser(withID id: User.ID) -> some Publisher<User, Error> {
// … data loading and decoding
}
}

Now, you no longer need force-casting at each call site, and manual type erasure is avoided. The compiler ensures full type safety.

UserLoader()
.loadUser(withID: userID)
.sink(receiveCompletion: { completion in
// … completion handling
}, receiveValue: { user in
// Properly typed User value passed into this closure.
})

Beyond Combine

This primary associated types feature isn’t limited to Combine but can be used with your own generic protocols as well. Consider a `Loadable` protocol for abstracting different ways of loading values:

protocol Loadable<Value> {
associatedtype Value
func load() async throws -> Value
}

With primary associated types, you can return a specific `Loadable` without revealing its underlying type:

func loadableForArticle(withID id: Article.ID) -> some Loadable<Article> {
if useLocalData {
return DatabaseLoadable(id: id)
}
let url = urlForLoadingArticle(withID: id)
return NetworkLoadable(url: url)
}

Dynamic Situations: Introducing the `any` Keyword

In situations where you need to switch between different implementations dynamically, you can use the `any` keyword. It tells the compiler to perform the required type erasure for you:

func loadableForArticle(withID id: Article.ID) -> any Loadable<Article> {
if useLocalData {
return DatabaseLoadable(id: id)
}
let url = urlForLoadingArticle(withID: id)
return NetworkLoadable(url: url)
}

However, using `any` turns the return value into an existential, which can have performance implications and limitations when working with certain generic APIs.

In conclusion, Swift 5.7 introduces primary associated types and enhances the `some` keyword to provide better type safety and eliminate the need for manual type erasure. These features make it easier to work with generic protocols, especially in frameworks like Combine and SwiftUI. Swift continues to evolve, making it more powerful and expressive for developers.

--

--

KD Knowledge Diet

Software Engineer, Mobile Developer living in Seoul. I hate people using difficult words. Why not using simple words? Keep It Simple Stupid!