Skip to Content
Course content

186: Primary Associated Types

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you've been working with protocols and associated types for a while, you know the "associated type" headache. For years, once we added an associatedtype to a protocol, that protocol stopped being a simple type we could pass around. We had to deal with some and any, or write those massive where clauses that make your code look more like a math textbook than a program.

Swift 5.7 introduced Primary Associated Types, and honestly, it's one of those changes that makes me wonder why we ever did it the hard way. It allows us to treat a protocol with associated types almost like a generic class or struct.

The struggle with generic protocols

Let's build a simple DataStore. I want a protocol that can save and retrieve a specific type of data, whether that's saved to a database, a file, or just held in memory for testing.

protocol DataStore {
    associatedtype Item
    func save(_ item: Item)
    func fetch() -> Item?
}

Now, here is where I usually run into a wall. I want to write a SyncManager that takes a DataStore and pushes data to it. In the past, I might have tried this:

class SyncManager {
    func performSync(store: any DataStore, item: String) {
        store.save(item) // ❌ Compiler Error!
    }
}

I'll admit, I still do this by reflex sometimes. The compiler screams at me because any DataStore is an existential type. It knows it's some kind of store, but it has no idea if the Item it expects is a String, an Int, or a User object. The types don't line up.

Cleaning things up with Primary Associated Types

To fix this, we can promote the associated type to a Primary Associated Type. We do this by adding the type in angle brackets right next to the protocol name. It looks like a generic, but it's actually just telling Swift: "This is the main type I'll be using to identify this protocol."

protocol DataStore<Item> {
    associatedtype Item
    func save(_ item: Item)
    func fetch() -> Item?
}

Notice that we still keep the associatedtype Item inside the body. The <Item> on the protocol declaration is just a "hint" for the type system. Now, look at how much cleaner our SyncManager becomes. We can specify exactly what kind of store we want using any DataStore<String>.

class SyncManager {
    func performSync(store: any DataStore<String>, item: String) {
        store.save(item) // ✅ This now works perfectly
    }
}

I love this because it removes the ambiguity. We aren't just saying "any store"; we're saying "any store that specifically handles Strings."

Applying this to real implementations

To make this concrete, let's implement a couple of different stores. The implementation side doesn't actually change—we still just define the Item type as we always have.

struct MemoryStore: DataStore {
    typealias Item = String
    private var storage: String?

    func save(_ item: String) {
        storage = item
    }

    func fetch() -> String? {
        return storage
    }
}

struct FileStore: DataStore {
    typealias Item = String
    
    func save(_ item: String) {
        print("Saving \(item) to disk...")
    }

    func fetch() -> String? {
        return "Data from disk"
    }
}

Now we can swap these out effortlessly. Since both conform to DataStore<String>, the SyncManager doesn't care which one it gets.

let manager = SyncManager()
let memStore = MemoryStore()
let fileStore = FileStore()

manager.performSync(store: memStore, item: "Hello Memory!")
manager.performSync(store: fileStore, item: "Hello Disk!")

By using Primary Associated Types, we've turned a clumsy protocol into a first-class generic citizen. You get the flexibility of protocols with the type safety of generics, without the syntactic noise of the old where clauses.




📋 Practical Task

Implementing a Generic Notification Dispatcher

You are building a notification system. You need a protocol called NotificationHandler that can handle a specific type of Payload.

Your task:

  • Define the NotificationHandler protocol using a Primary Associated Type for the Payload. It should have one method: func handle(payload: Payload).
  • Create a struct called EmailHandler that conforms to NotificationHandler where the Payload is a String.
  • Create a struct called AlertHandler that conforms to NotificationHandler where the Payload is an Int (representing an error code).
  • Write a function called dispatchNotification that takes two arguments: a handler (using the any keyword and the primary associated type) and a payload of the matching type.
  • Call this function twice: once with the EmailHandler and a string, and once with the AlertHandler and an integer.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.